Máscara de telefone e CEP que aceita colar e guarda só os dígitos
Monte campos de telefone e CEP que aceitam "+55 (11) 98888-7777" colado, formatam sem travar o teclado e mandam ao servidor só os números, validados de novo lá.
O cliente copia o próprio número da agenda do celular, "+55 11 98888-7777", e cola no campo de telefone do checkout. O campo mostra "(55) 1198-8887" e para de aceitar dígitos. Ele apaga, digita de novo, erra um número, e o entregador liga para um telefone que não existe. A resposta curta: a máscara é só visual; o que importa são os dígitos. Deixe a pessoa digitar ou colar qualquer coisa, extraia os números, normalize e formate quando ela sai do campo. No servidor, faça a mesma extração e a validação de verdade.
O exemplo deste guia é o checkout de uma loja online com dois campos: telefone para o entregador e CEP para o frete.
Por que a máscara clássica atrapalha
A maioria dos scripts de máscara foi escrita para quem digita um número de cada vez, da esquerda para a direita. Eles reagem a cada tecla, inserem o parêntese e o traço na posição esperada e bloqueiam qualquer tecla que não seja número. Funciona até a pessoa fazer algo diferente.
Cola é o caso mais comum. O texto colado chega inteiro, com espaço, sinal de mais e código do país, e o script tenta encaixar treze dígitos num molde de onze. Corrigir um dígito no meio é o segundo caso: a máscara reformata tudo e joga o cursor para o fim. O preenchimento automático do navegador é o terceiro, porque ele preenche o valor de uma vez, sem disparar as teclas que o script esperava.
Há ainda o problema do formato brasileiro. Celular tem 11 dígitos com DDD, fixo tem 10. A máscara que decide o formato no meio da digitação não sabe, no oitavo dígito, qual dos dois a pessoa está escrevendo.
Como montar campos que aceitam qualquer entrada
- Use o tipo e os atributos certos:
type="tel"para telefone etype="text"cominputmode="numeric"para CEP. Os dois abrem teclado numérico no celular. - Informe o preenchimento automático com
autocomplete="tel-national"eautocomplete="postal-code". - Não bloqueie teclas. Deixe entrar espaço, parêntese, traço e sinal de mais.
- Quando a pessoa sai do campo, extraia os dígitos, tire o código do país se ele veio, e formate.
- Se o número de dígitos não fecha, mostre a mensagem ao lado do campo. Não apague o que ela escreveu.
- No servidor, repita a extração, valide o tamanho e grave só os dígitos.
O HTML
<label for="telefone">Celular ou telefone com DDD</label>
<input type="tel" id="telefone" name="telefone" autocomplete="tel-national"
aria-describedby="telefone-erro">
<p id="telefone-erro" hidden></p>
<label for="cep">CEP</label>
<input type="text" id="cep" name="cep" inputmode="numeric" autocomplete="postal-code"
aria-describedby="cep-erro">
<p id="cep-erro" hidden></p>
Não há maxlength. Um limite de 15 caracteres, que parece suficiente para "(11) 98888-7777", corta o "+55 11 98888-7777" colado no meio, porque ele tem 17. Também não há type="number": CEP começa com zero em boa parte de São Paulo, como 01310-100, e campo numérico pode descartar o zero, além de mostrar setas de aumentar e diminuir que não fazem sentido para código.
O JavaScript
function soDigitos(texto) {
return texto.replace(/\D/g, '');
}
function normalizarTelefone(texto) {
let d = soDigitos(texto);
if ((d.length === 12 || d.length === 13) && d.startsWith('55')) {
d = d.slice(2);
}
return d;
}
function formatarTelefone(d) {
if (d.length === 11) return '(' + d.slice(0, 2) + ') ' + d.slice(2, 7) + '-' + d.slice(7);
if (d.length === 10) return '(' + d.slice(0, 2) + ') ' + d.slice(2, 6) + '-' + d.slice(6);
return d;
}
function formatarCep(d) {
return d.length === 8 ? d.slice(0, 5) + '-' + d.slice(5) : d;
}
function ligarCampo(campo, normalizar, formatar, tamanhos, mensagem) {
const erro = document.getElementById(campo.id + '-erro');
campo.addEventListener('blur', function () {
if (campo.value.trim() === '') return;
const d = normalizar(campo.value);
const valido = tamanhos.includes(d.length);
if (valido) campo.value = formatar(d);
campo.setAttribute('aria-invalid', valido ? 'false' : 'true');
erro.hidden = valido;
erro.textContent = valido ? '' : mensagem;
});
}
ligarCampo(document.getElementById('telefone'), normalizarTelefone, formatarTelefone,
[10, 11], 'Informe o DDD e o número, com 10 ou 11 dígitos.');
ligarCampo(document.getElementById('cep'), soDigitos, formatarCep,
[8], 'O CEP tem 8 dígitos.');
Siga o exemplo do começo. A pessoa cola "+55 11 98888-7777". Ao sair do campo, soDigitos devolve 5511988887777, com 13 dígitos. Como começa com 55 e tem 13, o código do país sai e sobra 11988887777, com 11. O campo passa a mostrar "(11) 98888-7777". A checagem do tamanho antes de cortar o 55 importa: existe DDD 55, no Rio Grande do Sul, e um número de lá tem 10 ou 11 dígitos, então não é confundido.
Formatar ao sair do campo, e não a cada tecla, evita o cursor pulando para o fim quando a pessoa corrige um dígito no meio. Se você quer formatar durante a digitação, precisa recalcular a posição do cursor depois de cada mudança, o que é bem mais código e bem mais fácil de errar.
No servidor
<?php
function soDigitos(string $texto): string
{
return preg_replace('/\D+/', '', $texto);
}
$telefone = soDigitos($_POST['telefone'] ?? '');
if ((strlen($telefone) === 12 || strlen($telefone) === 13) && str_starts_with($telefone, '55')) {
$telefone = substr($telefone, 2);
}
if (!in_array(strlen($telefone), [10, 11], true)) {
$erros['telefone'] = 'Informe o DDD e o número, com 10 ou 11 dígitos.';
}
$cep = soDigitos($_POST['cep'] ?? '');
if (strlen($cep) !== 8) {
$erros['cep'] = 'O CEP tem 8 dígitos.';
}
Aqui strlen é seguro, porque depois da extração só sobram dígitos ASCII. A função str_starts_with existe a partir do PHP 8. Grave 11988887777 e 01310100 numa coluna de texto, nunca numérica, para preservar o zero à esquerda. A formatação com parêntese e traço é responsabilidade de quem exibe. Para a diferença entre máscara e validação em outros campos numéricos, veja campo numérico: a máscara não é a validação.
Erros comuns
- Bloquear tudo que não é número. A cola com espaço ou parêntese é recusada inteira, e a pessoa precisa digitar dígito por dígito.
maxlengthdo tamanho formatado. O número colado com +55 é cortado no meio e o pedido sai com telefone errado.- Usar
type="number"para CEP. O zero à esquerda some e o frete é calculado para outro lugar, ou não é calculado. - Gravar o texto formatado. O banco fica com "(11) 98888-7777", "11 98888 7777" e "11988887777" para o mesmo número, e a busca por telefone no painel não acha nada.
- Coluna numérica para telefone ou CEP. O zero inicial é perdido na gravação, sem erro nenhum.
- Validar só no navegador. Aplicativo, importação e requisição direta gravam qualquer coisa.
Checklist
- Colar "+55 (11) 98888-7777" resulta em "(11) 98888-7777" ao sair do campo.
- Os campos abrem teclado numérico no celular e aceitam preenchimento automático.
- Nenhum
maxlengthcorta a cola com código do país. - O CEP 01310-100 chega ao banco com o zero.
- Corrigir um dígito no meio não joga o cursor para o fim.
- O servidor extrai dígitos, valida o tamanho e grava só números, em coluna de texto.
- Erro de tamanho aparece ao lado do campo, sem apagar o que foi digitado.
Cole o seu próprio número, com +55 e parênteses, no campo de telefone do seu formulário. Ele ficou inteiro? E no banco, a coluna guarda os dígitos limpos ou cada pedido com um formato diferente?
Referência: http://www.vivaolinux.com.br/script/Mascara-JavaScript-para-Campos-de-Telefone-Celular-e-CEP/