Transparência: sou fundador da CPFHub, citada no final. O componente funciona sem nenhuma API.
O campo de CPF parece trivial até você receber os primeiros tickets: "não consigo colar meu CPF", "o cursor pula para o final", "o teclado do celular abre com letras". Vamos construir um componente React que resolve tudo isso sem biblioteca de máscara.
1. Guarde dígitos, mostre máscara
O estado deve conter apenas dígitos. A máscara é derivada na renderização. Isso elimina a maior parte dos bugs.
const soDigitos = (s) => s.replace(/\D/g, "").slice(0, 11);
function mascararCpf(d) {
return d
.replace(/^(\d{3})(\d)/, "$1.$2")
.replace(/^(\d{3})\.(\d{3})(\d)/, "$1.$2.$3")
.replace(/\.(\d{3})(\d)/, ".$1-$2");
}
Teste rápido: mascararCpf("52998224725") → "529.982.247-25", e mascararCpf("5299") → "529.9". A máscara progressiva funciona enquanto o usuário digita.
2. Validação dos dígitos
export function cpfValido(d) {
if (!/^\d{11}$/.test(d) || /^(\d)\1{10}$/.test(d)) return false;
for (let t = 9; t < 11; t++) {
let soma = 0;
for (let i = 0; i < t; i++) soma += Number(d[i]) * (t + 1 - i);
if (Number(d[t]) !== ((soma * 10) % 11) % 10) return false;
}
return true;
}
3. O componente
import { useId, useState } from "react";
export function CampoCpf({ onValido }) {
const id = useId();
const [digitos, setDigitos] = useState("");
const [tocado, setTocado] = useState(false);
const completo = digitos.length === 11;
const valido = completo && cpfValido(digitos);
const erro = tocado && digitos.length > 0 && (!completo || !valido);
return (
<div>
<label htmlFor={id}>CPF</label>
<input
id={id}
name="cpf"
inputMode="numeric"
autoComplete="off"
placeholder="000.000.000-00"
value={mascararCpf(digitos)}
onChange={(e) => {
const d = soDigitos(e.target.value);
setDigitos(d);
if (d.length === 11 && cpfValido(d)) onValido?.(d);
}}
onBlur={() => setTocado(true)}
aria-invalid={erro}
aria-describedby={erro ? `${id}-erro` : undefined}
/>
{erro && (
<p id={`${id}-erro`} role="alert">
{completo ? "CPF inválido. Confira os números." : "O CPF tem 11 dígitos."}
</p>
)}
</div>
);
}
Decisões que importam:
-
inputMode="numeric"abre o teclado numérico no celular, mas, ao contrário detype="number", mantém o campo como texto (não perde zero à esquerda nem aceitae). - Erro só depois do blur. Mostrar "CPF inválido" enquanto a pessoa ainda digita o terceiro número é irritante.
-
Colar funciona porque normalizamos qualquer string com
soDigitos, inclusive529 982 247 25. -
aria-invalidearia-describedbyfazem o leitor de tela anunciar o erro.
4. E o cursor?
Como a máscara insere caracteres, editar no meio pode empurrar o cursor. Para a maioria dos formulários isso é aceitável, porque as pessoas digitam CPF do início ao fim. Se o seu caso exige edição no meio, calcule quantos dígitos estavam antes do cursor e reposicione em um useLayoutEffect. Ou use uma biblioteca madura como react-imask.
5. React Hook Form + Zod
Em formulários maiores, centralize a regra no schema:
import { z } from "zod";
const schema = z.object({
nome: z.string().min(3),
cpf: z
.string()
.transform((s) => s.replace(/\D/g, ""))
.refine(cpfValido, "CPF inválido"),
});
O transform garante que o backend recebe só dígitos.
6. Nunca confie só no front
Tudo isso é UX. O backend tem que validar de novo, porque qualquer um pode chamar sua API sem passar pelo formulário. E consultar uma API de CPF direto do navegador expõe sua chave: faça a consulta a partir de um endpoint seu (route handler, function, backend).
7. Pré-preenchendo o nome
Um uso comum: quando o CPF fica válido, o front chama o seu backend, que consulta uma API de CPF e devolve o primeiro nome para confirmação ("Você é Maria?"). Isso reduz digitação e erro. Mostre só o necessário e nunca a data de nascimento completa antes de a pessoa confirmar a identidade.
Sobre a CPFHub
Para esse backend, a CPFHub tem GET /cpf/{cpf} (header x-api-key), que retorna nome, gênero e data de nascimento em ~150 ms típicos (não SLA), com 50 créditos grátis por mês. Detalhes na documentação.
Top comments (0)