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.


