# Como exibir feedback visual de validação de CPF (cores, ícones, animações)

> Guia completo sobre como exibir feedback visual de validação de CPF usando cores, ícones e animações para melhor UX.

**Publicado:** 22/05/2025
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/como-exibir-feedback-visual-de-validacao-de-cpf-cores-icones-animacoes

---


Para exibir feedback visual de validação de CPF, combine cinco estados distintos — neutro, digitando, validando, válido e inválido — com cores semânticas, ícones inline e mensagens orientadoras. A abordagem correta nunca depende só da cor: sempre emparelha ícone + texto para garantir acessibilidade a usuários com daltonismo.

## Introdução

Quando um usuário digita seu CPF em um formulário, ele precisa saber rapidamente se o dado foi aceito, rejeitado ou está sendo processado. Feedback visual claro e imediato é a diferença entre uma experiência fluida e uma experiência frustrante. Sem feedback, o usuário fica na dúvida; com feedback ruim, ele fica confuso.

Cada exemplo neste guia é implementado com HTML, CSS e JavaScript, com integração à API do [**CPFHub.io**](https://www.cpfhub.io/) para validação em tempo real.

---

## Os cinco estados de um campo de CPF

Um campo de CPF bem projetado deve representar visualmente cinco estados:

1. **Neutro**: o campo está vazio ou com dados parciais.
2. **Digitando**: o usuário está preenchendo o campo.
3. **Validando**: a requisição à API está em andamento.
4. **Válido**: o CPF foi confirmado pela API.
5. **Inválido**: o CPF não foi encontrado ou tem formato incorreto.

Cada estado precisa de uma representação visual distinta usando cor, ícone e texto.

---

## Sistema de cores para feedback

### Escolha acessível de cores

As cores devem funcionar para pessoas com daltonismo. Nunca dependa exclusivamente da cor para comunicar o estado — sempre combine com texto e ícones:

```css
:root {
 /* Estado neutro */
 --cpf-neutral-border: #d1d5db;
 --cpf-neutral-bg: #ffffff;

 /* Digitando */
 --cpf-focus-border: #3b82f6;
 --cpf-focus-shadow: rgba(59, 130, 246, 0.15);

 /* Validando */
 --cpf-loading-border: #f59e0b;
 --cpf-loading-text: #92400e;

 /* Válido */
 --cpf-valid-border: #059669;
 --cpf-valid-bg: #ecfdf5;
 --cpf-valid-text: #065f46;

 /* Inválido */
 --cpf-invalid-border: #dc2626;
 --cpf-invalid-bg: #fef2f2;
 --cpf-invalid-text: #991b1b;
}

.cpf-input {
 width: 100%;
 padding: 12px 44px 12px 16px;
 border: 2px solid var(--cpf-neutral-border);
 border-radius: 8px;
 font-size: 16px;
 transition: border-color 0.2s ease, box-shadow 0.2s ease,
 background-color 0.2s ease;
 outline: none;
}

.cpf-input:focus {
 border-color: var(--cpf-focus-border);
 box-shadow: 0 0 0 4px var(--cpf-focus-shadow);
}

.cpf-input--loading {
 border-color: var(--cpf-loading-border);
}

.cpf-input--valid {
 border-color: var(--cpf-valid-border);
 background-color: var(--cpf-valid-bg);
}

.cpf-input--invalid {
 border-color: var(--cpf-invalid-border);
 background-color: var(--cpf-invalid-bg);
}
```

---

## Ícones inline no campo

Ícones posicionados dentro do campo de input são o feedback visual mais rápido de ser percebido:

```html
<div class="cpf-wrapper">
 <input
 type="text"
 id="cpf"
 class="cpf-input"
 placeholder="000.000.000-00"
 inputmode="numeric"
 maxlength="14"
 />
 <div class="cpf-icon" id="cpf-icon" aria-hidden="true"></div>
</div>
<div class="cpf-message" id="cpf-message" role="status" aria-live="polite"></div>
```

```css
.cpf-wrapper {
 position: relative;
}

.cpf-icon {
 position: absolute;
 right: 12px;
 top: 50%;
 transform: translateY(-50%);
 width: 24px;
 height: 24px;
 display: flex;
 align-items: center;
 justify-content: center;
}

/* Spinner de loading */
.cpf-icon--loading::after {
 content: "";
 width: 18px;
 height: 18px;
 border: 2px solid var(--cpf-loading-border);
 border-top-color: transparent;
 border-radius: 50%;
 animation: cpf-spin 0.6s linear infinite;
}

/* Checkmark de sucesso */
.cpf-icon--valid svg {
 width: 20px;
 height: 20px;
 color: var(--cpf-valid-border);
}

/* X de erro */
.cpf-icon--invalid svg {
 width: 20px;
 height: 20px;
 color: var(--cpf-invalid-border);
}

@keyframes cpf-spin {
 to {
 transform: rotate(360deg);
 }
}
```

---

## Implementação JavaScript completa

```javascript
var cpfInput = document.getElementById("cpf");
var cpfIcon = document.getElementById("cpf-icon");
var cpfMessage = document.getElementById("cpf-message");
var debounceTimer = null;

var ICONS = {
 loading: "",
 valid:
 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 13l4 4L19 7"/></svg>',
 invalid:
 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>',
};

cpfInput.addEventListener("input", function () {
 formatCpf(this);
 var digits = this.value.replace(/\D/g, "");

 if (digits.length < 11) {
 resetState();
 return;
 }

 if (debounceTimer) clearTimeout(debounceTimer);
 debounceTimer = setTimeout(function () {
 validateCpf(digits);
 }, 500);
});

function formatCpf(field) {
 var v = field.value.replace(/\D/g, "").slice(0, 11);
 if (v.length > 9)
 v = v.replace(/(\d{3})(\d{3})(\d{3})(\d{1,2})/, "$1.$2.$3-$4");
 else if (v.length > 6)
 v = v.replace(/(\d{3})(\d{3})(\d{1,3})/, "$1.$2.$3");
 else if (v.length > 3) v = v.replace(/(\d{3})(\d{1,3})/, "$1.$2");
 field.value = v;
}

function resetState() {
 cpfInput.className = "cpf-input";
 cpfIcon.className = "cpf-icon";
 cpfIcon.innerHTML = "";
 cpfMessage.textContent = "";
 cpfMessage.className = "cpf-message";
}

function setState(state, message) {
 cpfInput.className = "cpf-input cpf-input--" + state;
 cpfIcon.className = "cpf-icon cpf-icon--" + state;
 cpfIcon.innerHTML = ICONS[state] || "";
 cpfMessage.textContent = message;
 cpfMessage.className = "cpf-message cpf-message--" + state;
}

async function validateCpf(cpf) {
 setState("loading", "Validando CPF...");

 var controller = new AbortController();
 var timeoutId = setTimeout(function () {
 controller.abort();
 }, 10000);

 try {
 var response = await fetch("https://api.cpfhub.io/cpf/" + cpf, {
 headers: {
 "x-api-key": "SUA_CHAVE_DE_API",
 Accept: "application/json",
 },
 signal: controller.signal,
 });

 clearTimeout(timeoutId);
 var data = await response.json();

 if (data.success) {
 setState("valid", "CPF válido - " + data.data.name);
 } else {
 setState("invalid", "CPF não encontrado na base de dados.");
 }
 } catch (err) {
 clearTimeout(timeoutId);
 setState(
 "invalid",
 err.name === "AbortError"
 ? "Tempo de validação excedido."
 : "Erro ao validar. Tente novamente."
 );
 }
}
```

---

## Mensagens textuais claras

O texto do feedback é tão importante quanto a cor e o ícone. Boas mensagens são:

- **Específicas**: dizem exatamente o que aconteceu.
- **Orientadoras**: indicam o que o usuário deve fazer.
- **Humanas**: evitam jargão técnico.

### Exemplos de mensagens

| Estado | Mensagem ruim | Mensagem boa |
|---|---|---|
| Formato inválido | "Erro 422" | "CPF deve ter 11 dígitos. Verifique o número digitado." |
| Validando | "Aguarde..." | "Estamos validando seu CPF..." |
| Válido | "OK" | "CPF válido — Nome: João Silva" |
| Não encontrado | "Falha" | "CPF não encontrado. Verifique se digitou corretamente." |
| Erro de rede | "Error" | "Não foi possível validar agora. Tente novamente em instantes." |

---

## Estilos para mensagens

```css
.cpf-message {
 font-size: 13px;
 margin-top: 6px;
 min-height: 20px;
 display: flex;
 align-items: center;
 gap: 4px;
 opacity: 0;
 transform: translateY(-4px);
 transition: opacity 0.2s ease, transform 0.2s ease;
}

.cpf-message--loading,
.cpf-message--valid,
.cpf-message--invalid {
 opacity: 1;
 transform: translateY(0);
}

.cpf-message--loading {
 color: var(--cpf-loading-text);
}

.cpf-message--valid {
 color: var(--cpf-valid-text);
}

.cpf-message--invalid {
 color: var(--cpf-invalid-text);
}
```

---

## Animação de checkmark SVG

Uma animação de checkmark desenhado é mais satisfatória do que simplesmente mostrar um ícone estático:

```css
.cpf-icon--valid svg path {
 stroke-dasharray: 24;
 stroke-dashoffset: 24;
 animation: draw-check 0.4s ease forwards 0.1s;
}

@keyframes draw-check {
 to {
 stroke-dashoffset: 0;
 }
}
```

---

## Implementação em React

Para projetos React, encapsule toda a lógica em um componente:

```jsx
import React, { useState, useRef } from "react";

function CpfInput({ onValidated }) {
 const [value, setValue] = useState("");
 const [state, setState] = useState("neutral");
 const [message, setMessage] = useState("");
 const debounceRef = useRef(null);

 function formatCpf(raw) {
 var v = raw.replace(/\D/g, "").slice(0, 11);
 if (v.length > 9)
 return v.replace(/(\d{3})(\d{3})(\d{3})(\d{1,2})/, "$1.$2.$3-$4");
 if (v.length > 6)
 return v.replace(/(\d{3})(\d{3})(\d{1,3})/, "$1.$2.$3");
 if (v.length > 3) return v.replace(/(\d{3})(\d{1,3})/, "$1.$2");
 return v;
 }

 async function validate(cpf) {
 setState("loading");
 setMessage("Validando CPF...");

 const ctrl = new AbortController();
 const tid = setTimeout(() => ctrl.abort(), 10000);

 try {
 const res = await fetch("https://api.cpfhub.io/cpf/" + cpf, {
 headers: {
 "x-api-key": "SUA_CHAVE_DE_API",
 Accept: "application/json",
 },
 signal: ctrl.signal,
 });
 clearTimeout(tid);
 const data = await res.json();

 if (data.success) {
 setState("valid");
 setMessage("CPF válido - " + data.data.name);
 if (onValidated) onValidated(data.data);
 } else {
 setState("invalid");
 setMessage("CPF não encontrado.");
 }
 } catch {
 clearTimeout(tid);
 setState("invalid");
 setMessage("Erro na validação.");
 }
 }

 function handleChange(e) {
 const formatted = formatCpf(e.target.value);
 setValue(formatted);
 const digits = formatted.replace(/\D/g, "");

 if (digits.length < 11) {
 setState("neutral");
 setMessage("");
 return;
 }

 if (debounceRef.current) clearTimeout(debounceRef.current);
 debounceRef.current = setTimeout(() => validate(digits), 500);
 }

 return (
 <div className="cpf-field">
 <div className="cpf-wrapper">
 <input
 type="text"
 className={`cpf-input cpf-input--${state}`}
 value={value}
 onChange={handleChange}
 placeholder="000.000.000-00"
 inputMode="numeric"
 maxLength={14}
 />
 <div className={`cpf-icon cpf-icon--${state}`} aria-hidden="true" />
 </div>
 {message && (
 <div className={`cpf-message cpf-message--${state}`} role="status">
 {message}
 </div>
 )}
 </div>
 );
}

export default CpfInput;
```

---

## Acessibilidade

Feedback visual deve ser acompanhado de feedback acessível:

- Use `role="status"` e `aria-live="polite"` para mensagens.
- Nunca comunique estados apenas por cor.
- Mantenha contraste mínimo de 4.5:1 para texto.
- Respeite `prefers-reduced-motion` para animações.

```css
@media (prefers-reduced-motion: reduce) {
 .cpf-input,
 .cpf-message,
 .cpf-icon--loading::after {
 transition: none;
 animation: none;
 }
}
```

---

## Perguntas frequentes

### Qual é o tempo de resposta da API de CPF para o estado "validando" no frontend?
A API da CPFHub.io responde em aproximadamente 900ms. Para o usuário, esse intervalo deve ser preenchido com um spinner animado e a mensagem "Estamos validando seu CPF...". Use debounce de 300–500ms após o último dígito digitado para evitar chamadas desnecessárias enquanto o usuário ainda está digitando.

### Como garantir que o feedback de erro seja acessível para usuários com daltonismo?
Nunca dependa exclusivamente da cor para comunicar o estado. Combine sempre cor + ícone + texto: o estado inválido deve mostrar borda vermelha, ícone X e a mensagem "CPF não encontrado. Verifique se digitou corretamente." Use `aria-live="polite"` para que leitores de tela anunciem a mudança de estado automaticamente.

### A API bloqueia o frontend quando o limite de consultas é atingido?
Não. A API da CPFHub.io nunca interrompe o serviço — quando o limite mensal é atingido, ela continua respondendo e cobra R$0,15 por consulta adicional. O frontend continua exibindo feedback normalmente, sem erros inesperados de bloqueio. A [ANPD](https://www.gov.br/anpd) recomenda que o tratamento de dados seja informado ao usuário antes da consulta.

### Como implementar o feedback visual em formulários com múltiplos campos de CPF?
Encapsule a lógica em uma função ou componente reutilizável que receba o elemento de input como parâmetro. Cada instância gerencia seu próprio estado e timer de debounce independentemente. No React, o componente `CpfInput` acima já é reutilizável por padrão — basta instanciá-lo múltiplas vezes com diferentes callbacks `onValidated`.

### 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)
- [Como reduzir a fricção no onboarding digital usando validação de CPF](https://cpfhub.io/blog/como-reduzir-a-friccao-no-onboarding-digital-usando-validacao-de-cpf)
- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)
- [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)

---

## Conclusão

Feedback visual bem implementado transforma a validação de CPF de um ponto de atrito em uma experiência satisfatória. A combinação de cores semânticas, ícones claros, animações sutis e mensagens orientadoras cria um campo de CPF que o usuário confia e completa sem hesitar.

Com respostas em ~900ms e conformidade com a LGPD, a CPFHub.io fornece a base confiável para um feedback visual preciso e imediato.

Cadastre-se em [cpfhub.io](https://www.cpfhub.io/) — 50 consultas mensais gratuitas, sem cartão de crédito.

