Máscara e validação de CNPJ alfanumérico
Olá meus Unicórnios! 🦄✨
Em 31 de julho de 2026 o CNPJ ganha letras. 😳 Isso mesmo: o número que a gente valida com o mesmo código há anos passa a aceitar letras maiúsculas nas doze primeiras posições, e vira uma coisa assim:
12.ABC.345/01DE-35
Quando eu li isso, meu primeiro pensamento foi: "pronto, lá se vai toda máscara de CNPJ que eu já escrevi na vida". 😅 E o segundo foi pior — quantos formulários por aí vão simplesmente recusar um cadastro legítimo, porque o campo só aceita número?
Então fui atrás da regra oficial e escrevi o mínimo necessário: uma função que mascara e uma que valida, em cada linguagem — mais uma terceira, no JavaScript, que liga a máscara no seu campo com uma linha só. Sem biblioteca, sem instalação, sem classe. É para você copiar e colar no seu projeto hoje. 🎁
📅 O que muda, exatamente
Menos do que parece, e é por isso que dá para se preparar sem sofrimento. O CNPJ continua com 14 caracteres e continua com a mesma máscara de sempre. Muda só o conjunto de caracteres que cada posição aceita:
12.ABC.345/01DE-35
^^^^^^^^^^^^^^ ^^
12 primeiras 2 últimos
letra OU dígito só dígito
As doze primeiras posições aceitam letras maiúsculas e dígitos. Os dois dígitos verificadores do fim continuam sendo sempre numéricos. Guarde esse detalhe, porque ele vira uma linha da validação lá embaixo.
🎭 A máscara em JavaScript
Primeira função. Ela limpa o que a pessoa digitou e vai enfiando o separador na posição certa:
/**
* Aplica a mascara 00.000.000/0000-00 enquanto a pessoa digita.
*/
function mascararCnpj(texto) {
// toUpperCase antes de tudo: 'a' e 'A' tem valores diferentes na conta.
const limpo = String(texto || '')
.toUpperCase()
.replace(/[^0-9A-Z]/g, '')
.slice(0, 14);
let saida = '';
for (let i = 0; i < limpo.length; i++) {
if (i === 2) saida += '.';
if (i === 5) saida += '.';
if (i === 8) saida += '/';
if (i === 12) saida += '-';
saida += limpo[i];
}
return saida;
}
Repare no toUpperCase() dentro da função. Ele parece
cosmético — "ah, é só para ficar bonito na tela". Não é. 😳
Ele é metade de uma das armadilhas deste artigo, e eu volto nele daqui a
pouco.
⌨️ Ligando a máscara no campo — uma linha
A função acima mascara um texto, mas ninguém quer escrever
addEventListener toda vez. Então tem uma terceira que faz isso
por você: passe o id do input e pronto.
<input type="text" id="cnpj" inputmode="text"
autocapitalize="characters" maxlength="18"
placeholder="00.000.000/0000-00">
<script src="cnpj.js"></script>
<script>
aplicarMascaraCnpj('cnpj');
</script>
É isso. A máscara passa a acontecer a cada tecla. 🎉
Só que tem um detalhe que quase todo mundo erra — e eu errei também na
primeira versão. 😅 Reescrever o value joga o cursor
para o fim do campo. Enquanto você digita normalmente ninguém
percebe, porque o cursor já estava lá. Mas experimente voltar e corrigir uma
letra no meio do número: a cada tecla o cursor pula para o final e o que você
digita sai embaralhado.
Por isso a função guarda a posição antes de remascarar — e o jeito de
guardar é a parte esperta: em vez do índice bruto, ela conta
quantos caracteres úteis (letra ou dígito, ignorando
. / -) existem antes do cursor. Aí,
depois de remascarar, procura essa mesma quantidade no texto novo. Assim o
cursor acompanha certo mesmo quando a máscara acabou de inserir um
separador:
/**
* Liga a mascara num campo, pelo id. Chame uma vez e esqueca:
*
* aplicarMascaraCnpj('cnpj');
*/
function aplicarMascaraCnpj(id) {
const campo = document.getElementById(id);
if (!campo) return null;
campo.addEventListener('input', function () {
// Quantos caracteres uteis existem ANTES do cursor. Contando so estes
// (e ignorando . / -) o cursor volta para o lugar certo mesmo quando a
// mascara insere um separador. Sem isso, editar no meio do texto joga
// o cursor para o fim a cada tecla.
const cursor = campo.selectionStart;
const uteisAntes = campo.value.slice(0, cursor).replace(/[^0-9A-Za-z]/g, '').length;
campo.value = mascararCnpj(campo.value);
// Reposiciona: anda pelo texto novo ate ter passado a mesma quantidade
// de caracteres uteis.
let contados = 0;
let posicao = campo.value.length;
for (let i = 0; i < campo.value.length; i++) {
if (/[0-9A-Z]/.test(campo.value[i])) contados++;
if (contados === uteisAntes) { posicao = i + 1; break; }
}
if (uteisAntes === 0) posicao = 0;
campo.setSelectionRange(posicao, posicao);
});
return campo;
}
Confirmado no navegador: com o campo em 12.ABC.345, colocando
o cursor depois do 12. e digitando 9, o valor vira
12.9AB.C34/5 e o cursor fica logo depois do 9 — não
no fim. É um detalhe pequeno que separa o campo agradável do campo
irritante. 🙂
🔢 A conta: onde o A vale 17
Antes da segunda função, o pulo do gato. E é mais bonito do que eu esperava. 🥰
A regra oficial diz que o cálculo continua sendo módulo 11, com os mesmos pesos de sempre. O que muda é como cada caractere vira número: pega-se o código ASCII e subtrai 48.
'0' ASCII 48 -> 48 - 48 = 0
'9' ASCII 57 -> 57 - 48 = 9
'A' ASCII 65 -> 65 - 48 = 17
'B' ASCII 66 -> 66 - 48 = 18
'Z' ASCII 90 -> 90 - 48 = 42
Percebeu a sacada? 🤯 Os dígitos continuam valendo eles
mesmos. O '7' vale 7, como sempre valeu. É exatamente
por isso que a mesma função valida o CNPJ antigo e o novo, sem nenhum "se for
do formato velho, faz assim" no meio do código.
E repare que 'A' vale 17, não 10. Aquele
pulinho de 9 para 17 é o buraco que existe na tabela ASCII entre o
'9' e o 'A' — sete caracteres de pontuação
(:, ;, <, =,
>, ?, @) que ninguém pulou fora
porque a fórmula é literalmente "menos 48".
✅ A validação em JavaScript
Segunda função. É a conta acima dentro do módulo 11 de sempre:
/**
* Diz se o CNPJ e valido. Aceita com ou sem mascara, novo ou antigo.
*/
function validarCnpj(texto) {
const limpo = String(texto || '')
.toUpperCase()
.replace(/[^0-9A-Z]/g, '');
// 12 primeiros: letra ou digito. 2 ultimos: so digito.
if (!/^[0-9A-Z]{12}[0-9]{2}$/.test(limpo)) return false;
// 00000000000000 e afins passam no modulo 11, mas nao valem.
if (/^(.)\1{13}$/.test(limpo)) return false;
// Cada caractere vira "codigo ASCII menos 48": '0'..'9' -> 0..9 e
// 'A'..'Z' -> 17..42. E por isso que o CNPJ antigo continua valendo.
const calcularDigito = (base) => {
let soma = 0;
let peso = 2;
for (let i = base.length - 1; i >= 0; i--) {
soma += (base.charCodeAt(i) - 48) * peso;
peso = peso === 9 ? 2 : peso + 1;
}
const resto = soma % 11;
return resto < 2 ? 0 : 11 - resto;
};
const base = limpo.slice(0, 12);
const primeiro = calcularDigito(base);
const segundo = calcularDigito(base + primeiro);
return limpo === base + primeiro + segundo;
}
Três linhas merecem comentário:
A expressão /^[0-9A-Z]{12}[0-9]{2}$/ é onde aquele detalhe lá
de cima vira código: doze caracteres alfanuméricos, e depois dois
dígitos. Se alguém mandar 12ABC34501DEA5, com letra no
dígito verificador, cai aqui.
A de /^(.)\1{13}$/ derruba 00000000000000 e
companhia. Esses passam no módulo 11 — a conta fecha certinho — mas não são
CNPJ de ninguém.
E o segundo dígito é calculado sobre a base mais o primeiro dígito, com 13 caracteres. Errar isso é o bug clássico de todo validador de documento. 😅
🐘 As duas em PHP
A mesma coisa, função por função. A máscara:
/**
* Aplica a mascara 00.000.000/0000-00.
*/
function MascararCnpj($texto)
{
// strtoupper antes de tudo: 'a' e 'A' tem valores diferentes na conta.
$limpo = preg_replace('/[^0-9A-Z]/', '', strtoupper((string) $texto));
$limpo = substr($limpo, 0, 14);
$saida = '';
for ($i = 0; $i < strlen($limpo); $i++) {
if ($i === 2) $saida .= '.';
if ($i === 5) $saida .= '.';
if ($i === 8) $saida .= '/';
if ($i === 12) $saida .= '-';
$saida .= $limpo[$i];
}
return $saida;
}
E a validação:
/**
* Diz se o CNPJ e valido. Aceita com ou sem mascara, novo ou antigo.
*/
function ValidarCnpj($texto)
{
$limpo = preg_replace('/[^0-9A-Z]/', '', strtoupper((string) $texto));
// 12 primeiros: letra ou digito. 2 ultimos: so digito.
if (!preg_match('/^[0-9A-Z]{12}[0-9]{2}$/', $limpo)) {
return false;
}
// 00000000000000 e afins passam no modulo 11, mas nao valem.
if (preg_match('/^(.)\1{13}$/', $limpo)) {
return false;
}
$base = substr($limpo, 0, 12);
$primeiro = CnpjDigito($base);
$segundo = CnpjDigito($base . $primeiro);
return $limpo === $base . $primeiro . $segundo;
}
Aqui eu precisei de uma ajudante, porque em PHP não dá para declarar a função interna com a mesma elegância do JavaScript:
/**
* Calcula um digito verificador. Usada so pela ValidarCnpj().
*
* Cada caractere vira "codigo ASCII menos 48": '0'..'9' -> 0..9 e
* 'A'..'Z' -> 17..42. E por isso que o CNPJ antigo continua valendo.
*/
function CnpjDigito($base)
{
$soma = 0;
$peso = 2;
for ($i = strlen($base) - 1; $i >= 0; $i--) {
$soma += (ord($base[$i]) - 48) * $peso;
$peso = $peso === 9 ? 2 : $peso + 1;
}
$resto = $soma % 11;
return $resto < 2 ? 0 : 11 - $resto;
}
O ord() faz o papel do charCodeAt(), e o
strtoupper() o do toUpperCase(). De resto, é linha
por linha a mesma coisa.
😱 Armadilha 1: a minúscula que recusa CNPJ válido
Essa é a que eu mais gostei de descobrir, porque ela não dá erro. Não lança exceção, não aparece no console, não quebra nada. Só recusa cadastro de gente de verdade. 🤫
Lembra do toUpperCase() que eu pedi para você guardar? Tire
ele e veja o que acontece com o mesmo CNPJ digitado de dois
jeitos:
12ABC34501DE -> dígitos 35 ✓
12abc34501de -> dígitos 05 ✗
Confirmado rodando aqui: o mesmo documento, calculado com letra minúscula, dá outro dígito verificador. E a explicação está na tabela ASCII de novo:
'A' ASCII 65 -> 65 - 48 = 17
'a' ASCII 97 -> 97 - 48 = 49 <- 32 a mais
A fórmula "menos 48" não sabe o que é maiúscula. Ela obedece:
a vale 49, e a conta inteira sai por outro caminho. O resultado
é um CNPJ perfeitamente válido sendo recusado só porque a pessoa não estava
com o Caps Lock ligado. 😳
É por isso que as duas funções normalizam antes de qualquer outra coisa: nada pode calcular nada antes de o texto estar em maiúsculas.
📱 Armadilha 2: o teclado do celular que não tem letra
Essa é mais óbvia depois que alguém fala, e mesmo assim vai pegar muita gente. 😅
Todo campo de CNPJ que existe hoje tem alguma variação disto:
<!-- os dois travam o CNPJ novo -->
<input type="number">
<input type="text" inputmode="numeric">
No celular, os dois abrem o teclado numérico. E teclado numérico não tem letra — a pessoa simplesmente não consegue digitar o CNPJ dela. Não é validação recusando: é o campo impedindo de escrever.
O certo é type="text" com inputmode="text", como
no exemplo lá de cima. O autocapitalize="characters" ainda sobe
o teclado do celular já em maiúsculas, e o maxlength="18" conta
os separadores: 14 caracteres mais os dois pontos, a barra e o hífen.
E vale revisar o banco também — coluna CHAR(14) já serve, mas
se em algum lugar o CNPJ virou BIGINT para "economizar espaço",
esse lugar vai quebrar. 🙃
🧪 O que eu testei antes de publicar
Não quis publicar validador de documento na base do "deve funcionar". 😬 Então rodei três coisas.
1. CNPJ no formato antigo continua valendo. Montei CNPJs numéricos válidos (fictícios, mas com dígito verificador de verdade) e passei todos pela função nova:
11.222.333/0001-81 valido
12.345.678/0001-95 valido
98.765.432/0001-98 valido
11.122.233/0001-83 valido
55.566.677/0001-83 valido
19.283.746/0001-88 valido
Todos passaram. Esse teste é o mais importante de todos, porque é ele que te autoriza a trocar a função antiga pela nova sem medo de quebrar a base que já está no banco. E, de quebra, é a prova de que eu não errei os pesos. 😌
2. Dígito trocado tem que cair. Validador que aprova tudo
também "passa" no teste de cima. Então somei 1 ao último dígito de cada um
dos seis (...81 virou ...82, e assim por diante) e
conferi que todos os seis foram recusados.
3. PHP e JavaScript dão o mesmo resultado. Essa foi a que me deixou tranquila para publicar as duas versões: gerei 5.000 CNPJs alfanuméricos aleatórios em JavaScript e validei todos em PHP. Resultado: 5.000 válidos nos dois, zero divergências. Se as duas implementações discordassem em um caso que fosse, seria bem chato descobrir isso em produção. 😅
🎁 Copiando para o seu projeto
É só pegar as funções da sua linguagem e colar. Não tem
npm install, não tem Composer, não tem dependência nenhuma — o
repositório inteiro são dois arquivos de código, e o resto são exemplos de
uso. Tem também um exemplo.html que você abre direto no
navegador para ver a máscara funcionando.
No README tem também como reproduzir a armadilha da minúscula
de propósito: tira o toUpperCase(), roda o exemplo, e vê o CNPJ
válido sendo recusado na sua frente. Recomendo — dói mais e ensina melhor do
que ler sobre. 😄
Julho de 2026 ainda parece longe, mas formulário de cadastro é daquelas
coisas que ninguém lembra de revisar até alguém reclamar que não conseguiu se
inscrever. Se o seu campo de CNPJ ainda for type="number", agora
você já sabe. 🙃
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Leia também
Node.js: populando uma planilha no Google Sheets
Como escrever dados numa planilha do Google Sheets com Node.js, e as tres armadilhas que transformam dado certo em planilha errada.
Node.js: consultando CPF e CNPJ no SPC
Como consultar CPF e CNPJ no SPC Brasil com Node.js puro: envelope SOAP na mao, sem biblioteca, e as armadilhas do caminho.
PHP: faturando compras no BOnline (Portugal)
Como emitir uma fatura em Portugal pela API do BOnline com PHP: NIF, IVA por linha, o total ao cêntimo e o documento que não pode ser reenviado.