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.

Redação CPFHub.io
Redação CPFHub.io
··9 min de leitura
Como exibir feedback visual de validação de CPF (cores, ícones, animações)

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 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:

: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:

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

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

EstadoMensagem ruimMensagem 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

.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:

.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:

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.
@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 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.


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 — 50 consultas mensais gratuitas, sem cartão de crédito.

CPFHub.io

Pronto para integrar a API?

50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

Redação CPFHub.io

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.

WhatsAppFale conosco via WhatsApp