DEV Community

CPFHub
CPFHub

Posted on

Campo de CPF em React sem dor: máscara, validação em tempo real e acessibilidade

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");
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Decisões que importam:

  • inputMode="numeric" abre o teclado numérico no celular, mas, ao contrário de type="number", mantém o campo como texto (não perde zero à esquerda nem aceita e).
  • 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, inclusive 529 982 247 25.
  • aria-invalid e aria-describedby fazem 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"),
});
Enter fullscreen mode Exit fullscreen mode

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)