Pular para o conteúdo
JavaScript

Máscara e validação de CNPJ alfanumérico

Paloma Macetko
Ilustração de um unicórnio diante de um formulário mágico onde as letras A B C e os dígitos 3 6 2 se encaixam nos quadradinhos de uma máscara, com uma coruja de óculos aprovando com a varinha e um castelo ao fundo

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. 🎁

Exemplos_MascaraCNPJ no GitHubAs funções em JavaScript e PHP, prontas para copiar, com exemplos que rodam.github.com

📅 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.

Exemplos_MascaraCNPJ no GitHubAs funções em JavaScript e PHP, prontas para copiar, com exemplos que rodam e as armadilhas explicadas no README.github.com

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