# Como criar um componente reutilizável de input de CPF para design systems

> Aprenda a criar um componente reutilizável de input de CPF para design systems, com máscara, validação, temas e integração com API.

**Publicado:** 17/12/2025
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/como-criar-um-componente-reutilizavel-de-input-de-cpf-para-design-systems

---


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**](https://www.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 `onValidate` e `onLookup` para 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.

```typescript
// 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.

```tsx
// 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).

```css
/* 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)

```tsx
<CPFInput
 label="CPF do titular"
 required
 onValidate={(isValid, digits) => {
 console.log('Valido:', isValid, 'Digitos:', digits);
 }}
/>
```

### Com consulta automática via API

```tsx
<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

```tsx
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

```tsx
<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.

```tsx
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](https://www.gov.br/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.

### Leia também

- [Como pedir CPF no checkout sem espantar o cliente](https://cpfhub.io/blog/como-pedir-cpf-no-checkout-sem-espantar-o-cliente)
- [Diferença entre validação de CPF e consulta de CPF: quando usar cada uma](https://cpfhub.io/blog/diferenca-entre-validacao-de-cpf-e-consulta-de-cpf-quando-usar-cada-uma)
- [Como evitar chargebacks usando validação de CPF no checkout](https://cpfhub.io/blog/como-evitar-chargebacks-usando-validacao-de-cpf-no-checkout)
- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)

---

## 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**](https://www.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](https://www.cpfhub.io/) — 50 consultas mensais gratuitas, sem cartão de crédito — e comece hoje mesmo.

