Criar um componente reutilizável de input de CPF para design systems garante que todos os produtos da empresa usem a mesma implementação — com máscara, validação local de dígitos verificadores e integração opcional com a API da CPFHub.io para consulta em tempo real. O resultado é consistência visual e técnica entre apps mobile, painéis web e totens de atendimento, sem reescrever a mesma lógica em cada projeto.
Introdução
Design systems são a espinha dorsal de produtos digitais escaláveis. Quando sua empresa possui múltiplos produtos -- um app mobile, um painel web, um totem de atendimento -- todos precisam de um campo de CPF que funcione de forma consistente, acessível e visualmente coerente.
Criar um componente de input de CPF reutilizável para seu design system garante que todos os times usem a mesma implementação, com as mesmas regras de validação, a mesma máscara e a mesma integração com a API da CPFHub.io, eliminando retrabalho e inconsistências entre produtos.
Requisitos do componente
Antes de começar a codificar, vamos definir os requisitos que um componente de CPF de design system deve atender:
Funcionais
- Máscara automática no formato
000.000.000-00. - Validação de dígitos verificadores.
- Suporte a consulta via API (opcional).
- Callback
onValidateeonLookuppara comunicação com o formulário pai.
Não funcionais
- Acessível (WCAG 2.1 AA).
- Estilizável via tokens de design / CSS variables.
- Compatível com bibliotecas de formulário (React Hook Form, Formik).
- Documentado com exemplos de uso.
Definindo a interface TypeScript
A tipagem clara é fundamental para componentes de design system, pois facilita a adoção por outros desenvolvedores.
// types.ts
export type CPFInputStatus = 'idle' | 'typing' | 'validating' | 'valid' | 'invalid' | 'loading' | 'error';
export interface CPFLookupResult {
cpf: string;
name: string;
nameUpper: string;
gender: string;
birthDate: string;
day: string;
month: string;
year: string;
}
export interface CPFInputProps {
/** Valor controlado do input (com máscara) */
value?: string;
/** Callback quando o valor muda */
onChange?: (maskedValue: string, rawDigits: string) => void;
/** Callback quando a validação local ocorre */
onValidate?: (isValid: boolean, digits: string) => void;
/** Callback quando a consulta à API retorna */
onLookup?: (result: CPFLookupResult | null, error?: string) => void;
/** Chave da API para consulta (se não fornecida, apenas validação local) */
apiKey?: string;
/** Timeout para consulta em ms (padrão: 10000) */
timeout?: number;
/** Habilitar consulta automática ao completar 11 dígitos */
autoLookup?: boolean;
/** Label do campo */
label?: string;
/** Texto de ajuda */
helpText?: string;
/** Tamanho do componente */
size?: 'sm' | 'md' | 'lg';
/** Desabilitar o campo */
disabled?: boolean;
/** Campo obrigatório */
required?: boolean;
/** ID para acessibilidade */
id?: string;
/** Classes CSS adicionais */
className?: string;
}
Implementando o componente
Com a interface definida, vamos implementar o componente.
// CPFInput.tsx
import React, { useState, useCallback, useRef, useEffect, forwardRef } from 'react';
import type { CPFInputProps, CPFInputStatus } from './types';
import styles from './CPFInput.module.css';
function maskCPF(value: string): string {
const digits = value.replace(/\D/g, '').slice(0, 11);
let result = '';
for (let i = 0; i < digits.length; i++) {
if (i === 3 || i === 6) result += '.';
if (i === 9) result += '-';
result += digits[i];
}
return result;
}
function validateCPFDigits(cpf: string): boolean {
if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false;
let sum = 0;
for (let i = 0; i < 9; i++) sum += parseInt(cpf[i]) * (10 - i);
let remainder = (sum * 10) % 11;
if (remainder === 10) remainder = 0;
if (remainder !== parseInt(cpf[9])) return false;
sum = 0;
for (let i = 0; i < 10; i++) sum += parseInt(cpf[i]) * (11 - i);
remainder = (sum * 10) % 11;
if (remainder === 10) remainder = 0;
return remainder === parseInt(cpf[10]);
}
export const CPFInput = forwardRef<HTMLInputElement, CPFInputProps>(({
value: controlledValue,
onChange,
onValidate,
onLookup,
apiKey,
timeout = 10000,
autoLookup = false,
label = 'CPF',
helpText,
size = 'md',
disabled = false,
required = false,
id = 'cpf-input',
className = ''
}, ref) => {
const [internalValue, setInternalValue] = useState('');
const [status, setStatus] = useState<CPFInputStatus>('idle');
const [message, setMessage] = useState('');
const controllerRef = useRef<AbortController | null>(null);
const displayValue = controlledValue !== undefined ? controlledValue : internalValue;
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const masked = maskCPF(e.target.value);
const digits = masked.replace(/\D/g, '');
if (controlledValue === undefined) {
setInternalValue(masked);
}
onChange?.(masked, digits);
if (digits.length < 11) {
setStatus('typing');
setMessage('');
return;
}
const isValid = validateCPFDigits(digits);
onValidate?.(isValid, digits);
if (!isValid) {
setStatus('invalid');
setMessage('CPF invalido. Verifique os digitos.');
return;
}
setStatus('valid');
setMessage('');
if (autoLookup && apiKey) {
performLookup(digits);
}
}, [controlledValue, onChange, onValidate, autoLookup, apiKey]);
async function performLookup(digits: string) {
setStatus('loading');
setMessage('Consultando...');
if (controllerRef.current) controllerRef.current.abort();
controllerRef.current = new AbortController();
const timeoutId = setTimeout(() => controllerRef.current?.abort(), timeout);
try {
const res = await fetch(`https://api.cpfhub.io/cpf/${digits}`, {
headers: {
'x-api-key': apiKey!,
'Accept': 'application/json'
},
signal: controllerRef.current.signal
});
clearTimeout(timeoutId);
const json = await res.json();
if (json.success) {
setStatus('valid');
setMessage(`${json.data.name}`);
onLookup?.(json.data);
} else {
setStatus('error');
setMessage('CPF nao encontrado.');
onLookup?.(null, 'not_found');
}
} catch (err: any) {
clearTimeout(timeoutId);
setStatus('error');
const errorMsg = err.name === 'AbortError' ? 'Tempo esgotado.' : 'Erro na consulta.';
setMessage(errorMsg);
onLookup?.(null, errorMsg);
}
}
useEffect(() => {
return () => {
if (controllerRef.current) controllerRef.current.abort();
};
}, []);
const statusClass = styles[`status_${status}`] || '';
const sizeClass = styles[`size_${size}`] || '';
return (
<div className={`${styles.container} ${className}`}>
<label htmlFor={id} className={styles.label}>
{label}
{required && <span className={styles.required}> *</span>}
</label>
<div className={styles.inputWrapper}>
<input
ref={ref}
id={id}
type="text"
inputMode="numeric"
placeholder="000.000.000-00"
value={displayValue}
onChange={handleChange}
maxLength={14}
disabled={disabled}
required={required}
autoComplete="off"
aria-invalid={status === 'invalid' || status === 'error'}
aria-describedby={`${id}-feedback`}
className={`${styles.input} ${statusClass} ${sizeClass}`}
/>
{status === 'loading' && <span className={styles.spinner} />}
{status === 'valid' && <span className={styles.checkmark}>{'\u2713'}</span>}
{(status === 'invalid' || status === 'error') && (
<span className={styles.errorIcon}>{'\u2717'}</span>
)}
</div>
{message && (
<p
id={`${id}-feedback`}
role="alert"
className={`${styles.feedback} ${
status === 'valid' ? styles.feedbackSuccess : styles.feedbackError
}`}
>
{message}
</p>
)}
{helpText && !message && (
<p id={`${id}-feedback`} className={styles.helpText}>
{helpText}
</p>
)}
</div>
);
});
CPFInput.displayName = 'CPFInput';
export default CPFInput;
Estilos com CSS variables
Para que o componente seja estilizável por diferentes temas, utilizamos CSS custom properties (variables).
/* CPFInput.module.css */
.container {
--cpf-font-family: var(--ds-font-family, 'Inter', sans-serif);
--cpf-border-color: var(--ds-border-color, #d1d5db);
--cpf-border-radius: var(--ds-border-radius, 8px);
--cpf-focus-color: var(--ds-focus-color, #3b82f6);
--cpf-success-color: var(--ds-success-color, #10b981);
--cpf-error-color: var(--ds-error-color, #ef4444);
--cpf-text-color: var(--ds-text-color, #1f2937);
--cpf-label-color: var(--ds-label-color, #374151);
--cpf-help-color: var(--ds-help-color, #6b7280);
font-family: var(--cpf-font-family);
}
.label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: var(--cpf-label-color);
margin-bottom: 4px;
}
.required { color: var(--cpf-error-color); }
.inputWrapper { position: relative; }
.input {
width: 100%;
border: 2px solid var(--cpf-border-color);
border-radius: var(--cpf-border-radius);
color: var(--cpf-text-color);
outline: none;
transition: border-color 0.2s;
padding-right: 40px;
}
.input:focus { border-color: var(--cpf-focus-color); }
.input:disabled { opacity: 0.5; cursor: not-allowed; }
.size_sm { padding: 8px 12px; font-size: 0.875rem; }
.size_md { padding: 10px 14px; font-size: 1rem; }
.size_lg { padding: 14px 18px; font-size: 1.125rem; }
.status_valid { border-color: var(--cpf-success-color); }
.status_invalid, .status_error { border-color: var(--cpf-error-color); }
.spinner {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
border: 2px solid var(--cpf-border-color);
border-top-color: var(--cpf-focus-color);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: translateY(-50%) rotate(360deg); } }
.checkmark, .errorIcon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 1.1rem;
}
.checkmark { color: var(--cpf-success-color); }
.errorIcon { color: var(--cpf-error-color); }
.feedback {
font-size: 0.8rem;
margin-top: 4px;
min-height: 20px;
}
.feedbackSuccess { color: var(--cpf-success-color); }
.feedbackError { color: var(--cpf-error-color); }
.helpText { font-size: 0.8rem; color: var(--cpf-help-color); margin-top: 4px; }
Exemplos de uso
Uso básico (apenas validação local)
<CPFInput
label="CPF do titular"
required
onValidate={(isValid, digits) => {
console.log('Valido:', isValid, 'Digitos:', digits);
}}
/>
Com consulta automática via API
<CPFInput
label="CPF"
apiKey={process.env.REACT_APP_CPFHUB_API_KEY}
autoLookup
timeout={10000}
onLookup={(result, error) => {
if (result) {
setNome(result.name);
setNascimento(result.birthDate);
}
}}
/>
Integração com React Hook Form
import { useForm, Controller } from 'react-hook-form';
function MeuFormulario() {
const { control, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="cpf"
control={control}
rules={{ required: true }}
render={({ field }) => (
<CPFInput
value={field.value}
onChange={(masked) => field.onChange(masked)}
ref={field.ref}
label="CPF"
required
/>
)}
/>
</form>
);
}
Tamanhos diferentes
<CPFInput size="sm" label="CPF (pequeno)" />
<CPFInput size="md" label="CPF (medio)" />
<CPFInput size="lg" label="CPF (grande)" />
Testes do componente
Um componente de design system precisa de cobertura de testes robusta.
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import CPFInput from './CPFInput';
describe('CPFInput', () => {
it('renderiza com label', () => {
render(<CPFInput label="CPF do cliente" />);
expect(screen.getByLabelText('CPF do cliente')).toBeInTheDocument();
});
it('aplica mascara automaticamente', () => {
render(<CPFInput />);
const input = screen.getByPlaceholderText('000.000.000-00');
fireEvent.change(input, { target: { value: '12345678901' } });
expect(input).toHaveValue('123.456.789-01');
});
it('chama onValidate com true para CPF valido', () => {
const onValidate = jest.fn();
render(<CPFInput onValidate={onValidate} />);
const input = screen.getByPlaceholderText('000.000.000-00');
fireEvent.change(input, { target: { value: '52998224725' } });
expect(onValidate).toHaveBeenCalledWith(true, '52998224725');
});
it('marca aria-invalid para CPF invalido', () => {
render(<CPFInput />);
const input = screen.getByPlaceholderText('000.000.000-00');
fireEvent.change(input, { target: { value: '11111111111' } });
expect(input).toHaveAttribute('aria-invalid', 'true');
});
it('respeita prop disabled', () => {
render(<CPFInput disabled />);
expect(screen.getByPlaceholderText('000.000.000-00')).toBeDisabled();
});
});
Documentação para o design system
Todo componente de design system precisa de documentação clara. Inclua no Storybook ou na documentação interna:
- Quando usar -- formulários que exigem CPF como input.
- Quando não usar -- quando o CPF é exibido como texto estático (use um componente de display).
- Props -- tabela completa com tipo, padrão e descrição.
- Exemplos visuais -- cada estado (idle, typing, valid, invalid, loading, error).
- Acessibilidade -- como o componente se comporta com leitores de tela e navegação por teclado.
A ANPD recomenda documentar a finalidade do tratamento de dados pessoais como o CPF, o que torna essa seção de documentação também relevante para conformidade com a LGPD.
Perguntas frequentes
O que é necessário para implementar validação de CPF neste contexto?
A validação de CPF exige uma chamada à API com o número do documento e a chave de autenticação. A CPFHub.io retorna o status do CPF, nome do titular e data de nascimento em cerca de 900ms, permitindo a verificação em tempo real durante o cadastro ou transação.
A API CPFHub.io funciona para todos os volumes de consulta?
Sim. O plano gratuito oferece 50 consultas por mês sem cartão de crédito — ideal para testes e projetos pequenos. Para volumes maiores, o plano Pro inclui 1.000 consultas mensais por R$149. Se o limite for ultrapassado, a API não bloqueia: cobra R$0,15 por consulta adicional.
Como garantir conformidade com a LGPD ao usar uma API de CPF?
Use o CPF apenas para a finalidade declarada ao titular, armazene apenas o necessário (não guarde o CPF cru se um token bastar), implemente controle de acesso aos logs de consulta e documente a base legal para o tratamento. A ANPD orienta que dados de identificação devem ser tratados com o princípio da necessidade.
Quanto tempo leva para integrar a API CPFHub.io?
A integração básica leva menos de 30 minutos: crie uma conta em cpfhub.io, gere a API key no painel e faça uma chamada GET para https://api.cpfhub.io/cpf/{CPF} com o header x-api-key. A documentação inclui exemplos em Python, Node.js, PHP, Java e outras linguagens.
Conclusão
Um componente reutilizável de input de CPF é um investimento que paga dividendos em todos os produtos da empresa. Ao centralizar máscara, validação e integração com API em um único componente, você garante consistência, reduz bugs e acelera o desenvolvimento de novos fluxos.
A integração com a API da CPFHub.io adiciona a camada de consulta em tempo real, preenchendo automaticamente nome e data de nascimento ao completar o CPF e eliminando erros de digitação nos cadastros.
Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e comece hoje mesmo.
CPFHub.io
Pronto para integrar a API?
50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.
Sobre a redação
Redação CPFHub.io
Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.



