Como implementar drag and drop de documento de CPF para upload e validação

Aprenda a implementar drag and drop de documentos de CPF para upload e validação automática, com interface visual e integração com API.

Redação CPFHub.io
Redação CPFHub.io
··11 min de leitura
Como implementar drag and drop de documento de CPF para upload e validação

Para implementar drag and drop de documento de CPF, você precisa de três camadas: uma zona de drop em HTML com tratamento dos eventos dragenter, dragleave e drop; OCR no navegador com Tesseract.js para extrair o número do documento; e uma chamada à API CPFHub.io com o CPF encontrado para validar os dados do titular. O resultado chega em cerca de 900ms, sem enviar a imagem para o servidor.

O drag and drop é uma das interações mais intuitivas em interfaces desktop. Arrastar um arquivo do computador e soltá-lo em uma área da página elimina a necessidade de navegar por diálogos de seleção de arquivo, tornando o processo mais fluido e natural.

No contexto de verificação de CPF, o drag and drop permite que o usuário arraste uma foto ou scan do documento (cartão CPF, RG, CNH) diretamente para a interface, que então extrai o número do CPF automaticamente e valida via API. Segundo as diretrizes de acessibilidade do W3C WCAG 2.1, toda interação baseada em gestos de apontamento deve ter uma alternativa acessível por teclado — o botão "Selecionar arquivo" cumpre esse requisito.

Anatomia da zona de drop

Uma zona de drop bem projetada comunica claramente três estados:

  1. Idle — estado padrão, com instruções sobre o que arrastar.
  2. Drag over — quando o arquivo está sendo arrastado sobre a zona.
  3. Processing — quando o arquivo foi solto e está sendo processado.

Estrutura HTML

<div class="drop-zone" id="dropZone">
    <div class="drop-idle" id="dropIdle">
    <div class="drop-icon">
    <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.5">
    <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
    <polyline points="17 8 12 3 7 8" />
    <line x1="12" y1="3" x2="12" y2="15" />
    </svg>
    </div>
    <p class="drop-title">Arraste seu documento aqui</p>
    <p class="drop-subtitle">CPF, RG ou CNH — JPG, PNG ou PDF</p>
    <span class="drop-separator">ou</span>
    <label class="btn-selecionar">
    Selecionar arquivo
    <input type="file" id="fileInput" accept="image/*,.pdf" style="display: none;" />
    </label>
    </div>

    <div class="drop-hover" id="dropHover" style="display: none;">
    <div class="drop-icon-animated">
    <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#3498db" stroke-width="2">
    <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
    <polyline points="7 10 12 15 17 10" />
    <line x1="12" y1="15" x2="12" y2="3" />
    </svg>
    </div>
    <p class="drop-title" style="color: #3498db;">Solte o arquivo aqui</p>
    </div>

    <div class="drop-processing" id="dropProcessing" style="display: none;">
    <div class="spinner-large"></div>
    <p class="drop-title" id="processingText">Analisando documento...</p>
    <div class="progress-bar">
    <div class="progress-fill" id="progressFill"></div>
    </div>
    </div>
</div>

<div class="resultado-container" id="resultadoContainer" style="display: none;">
    <div class="preview-wrapper">
    <img id="previewImagem" alt="Preview do documento" />
    </div>
    <div class="resultado-dados">
    <h3>CPF detectado</h3>
    <p class="cpf-detectado" id="cpfDetectado"></p>
    <div class="dados-api" id="dadosApi" style="display: none;">
    <p id="nomeResultado"></p>
    <p id="nascResultado"></p>
    </div>
    <div class="resultado-acoes">
    <button class="btn-confirmar" id="btnConfirmar" onclick="confirmarCPF()">
    Confirmar
    </button>
    <button class="btn-refazer" onclick="resetar()">Tentar novamente</button>
    </div>
    </div>
</div>

Estilos CSS com estados visuais

.drop-zone {
    max-width: 500px;
    margin: 20px auto;
    border: 2px dashed #ccc;
    border-radius: 16px;
    padding: 40px;
    text-align: center;
    transition: all 0.3s ease;
    background: #fafafa;
    cursor: pointer;
}
.drop-zone.hover {
    border-color: #3498db;
    background: #f0f7ff;
    transform: scale(1.02);
}
.drop-zone.processing {
    border-style: solid;
    border-color: #3498db;
    background: #fff;
    cursor: default;
}
.drop-zone.error {
    border-color: #e74c3c;
    background: #fff5f5;
}

.drop-icon { margin-bottom: 16px; }
.drop-title { font-size: 1.1rem; font-weight: 600; color: #333; margin-bottom: 4px; }
.drop-subtitle { font-size: 0.9rem; color: #999; margin-bottom: 12px; }
.drop-separator { display: block; color: #ccc; margin: 12px 0; font-size: 0.85rem; }

.btn-selecionar {
    display: inline-block;
    padding: 10px 24px;
    background: #3498db;
    color: #fff;
    border-radius: 8px;
    cursor: pointer;
    font-size: 0.95rem;
    transition: background 0.2s;
}
.btn-selecionar:hover { background: #2980b9; }

.drop-icon-animated svg {
    animation: bounce 0.6s ease infinite;
}
@keyframes bounce {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(8px); }
}

.spinner-large {
    width: 40px; height: 40px;
    border: 3px solid #ddd;
    border-top-color: #3498db;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    margin: 0 auto 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }

.progress-bar {
    width: 100%;
    height: 4px;
    background: #eee;
    border-radius: 2px;
    margin-top: 16px;
    overflow: hidden;
}
.progress-fill {
    height: 100%;
    background: #3498db;
    border-radius: 2px;
    width: 0%;
    transition: width 0.3s ease;
}

.resultado-container {
    max-width: 500px;
    margin: 20px auto;
    display: flex;
    gap: 20px;
    background: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 16px;
    padding: 20px;
}
.preview-wrapper {
    width: 160px;
    flex-shrink: 0;
}
.preview-wrapper img {
    width: 100%;
    border-radius: 8px;
    border: 1px solid #eee;
}
.resultado-dados { flex: 1; }
.cpf-detectado {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 1px;
    color: #2c3e50;
    margin: 8px 0 12px;
}
.dados-api p { font-size: 0.9rem; color: #555; margin-bottom: 4px; }
.resultado-acoes { margin-top: 16px; display: flex; gap: 8px; }
.btn-confirmar {
    padding: 10px 20px;
    background: #2ecc71;
    color: #fff;
    border: none;
    border-radius: 8px;
    cursor: pointer;
}
.btn-refazer {
    padding: 10px 20px;
    background: transparent;
    color: #666;
    border: 1px solid #ddd;
    border-radius: 8px;
    cursor: pointer;
}

@media (max-width: 600px) {
    .resultado-container { flex-direction: column; }
    .preview-wrapper { width: 100%; }
}

JavaScript de drag and drop

const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const dropIdle = document.getElementById('dropIdle');
const dropHover = document.getElementById('dropHover');
const dropProcessing = document.getElementById('dropProcessing');

// Prevenir comportamento padrao do navegador
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evento => {
    dropZone.addEventListener(evento, (e) => {
    e.preventDefault();
    e.stopPropagation();
    });
});

// Estado hover
dropZone.addEventListener('dragenter', () => {
    dropZone.classList.add('hover');
    dropIdle.style.display = 'none';
    dropHover.style.display = 'block';
});

dropZone.addEventListener('dragleave', (e) => {
    // Verificar se realmente saiu da zona (e nao de um filho)
    if (!dropZone.contains(e.relatedTarget)) {
    dropZone.classList.remove('hover');
    dropIdle.style.display = 'block';
    dropHover.style.display = 'none';
    }
});

// Drop
dropZone.addEventListener('drop', (e) => {
    dropZone.classList.remove('hover');
    const files = e.dataTransfer.files;
    if (files.length > 0) {
    processarArquivo(files[0]);
    }
});

// Click para selecionar arquivo
dropZone.addEventListener('click', (e) => {
    if (e.target.closest('.btn-selecionar')) return; // Label ja abre o input
    fileInput.click();
});

fileInput.addEventListener('change', (e) => {
    if (e.target.files.length > 0) {
    processarArquivo(e.target.files[0]);
    }
});

function validarArquivo(file) {
    const tiposPermitidos = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
    if (!tiposPermitidos.includes(file.type)) {
    return 'Formato nao suportado. Use JPG, PNG ou PDF.';
    }
    const maxSize = 10 * 1024 * 1024; // 10MB
    if (file.size > maxSize) {
    return 'Arquivo muito grande. O limite e 10MB.';
    }
    return null;
}

async function processarArquivo(file) {
    const erro = validarArquivo(file);
    if (erro) {
    exibirErro(erro);
    return;
    }

    // Mudar para estado de processamento
    dropIdle.style.display = 'none';
    dropHover.style.display = 'none';
    dropProcessing.style.display = 'block';
    dropZone.classList.add('processing');

    const progressFill = document.getElementById('progressFill');
    const processingText = document.getElementById('processingText');

    progressFill.style.width = '20%';
    processingText.textContent = 'Lendo documento...';

    try {
    // Carregar imagem
    const imageUrl = await lerComoDataURL(file);
    progressFill.style.width = '40%';

    // Preview
    document.getElementById('previewImagem').src = imageUrl;

    // OCR
    processingText.textContent = 'Extraindo texto...';
    progressFill.style.width = '60%';

    const texto = await executarOCR(imageUrl);
    progressFill.style.width = '80%';

    // Encontrar CPF
    processingText.textContent = 'Procurando CPF...';
    const cpf = encontrarCPF(texto);

    if (!cpf) {
    exibirErro('Nao foi possivel encontrar um CPF no documento. Tente com melhor iluminacao.');
    return;
    }

    progressFill.style.width = '90%';
    processingText.textContent = 'Verificando CPF...';

    // Consultar API
    await consultarEExibir(cpf);
    progressFill.style.width = '100%';

    } catch (err) {
    exibirErro('Erro ao processar documento. Tente novamente.');
    }
}

function lerComoDataURL(file) {
    return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
    });
}

async function executarOCR(imageUrl) {
    // Usando Tesseract.js (incluir via CDN)
    const worker = await Tesseract.createWorker('por');
    const { data: { text } } = await worker.recognize(imageUrl);
    await worker.terminate();
    return text;
}

function encontrarCPF(texto) {
    const limpo = texto.replace(/[oO]/g, '0').replace(/[lI]/g, '1');
    const padroes = [
    /(\d{3})[.\s]?(\d{3})[.\s]?(\d{3})[-.\s]?(\d{2})/g,
    /(\d{11})/g
    ];
    for (const padrao of padroes) {
    const match = limpo.match(padrao);
    if (match) {
    for (const candidato of match) {
    const digits = candidato.replace(/\D/g, '');
    if (digits.length === 11 && validarDigitosCPF(digits)) {
    return digits;
    }
    }
    }
    }
    return null;
}

function validarDigitosCPF(cpf) {
    if (/^(\d)\1{10}$/.test(cpf)) return false;
    let soma = 0;
    for (let i = 0; i < 9; i++) soma += parseInt(cpf[i]) * (10 - i);
    let resto = (soma * 10) % 11;
    if (resto === 10) resto = 0;
    if (resto !== parseInt(cpf[9])) return false;
    soma = 0;
    for (let i = 0; i < 10; i++) soma += parseInt(cpf[i]) * (11 - i);
    resto = (soma * 10) % 11;
    if (resto === 10) resto = 0;
    return resto === parseInt(cpf[10]);
}

async function consultarEExibir(digits) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10000);

    try {
    const res = await fetch(`https://api.cpfhub.io/cpf/${digits}`, {
    headers: {
    'x-api-key': 'SUA_CHAVE_DE_API',
    'Accept': 'application/json'
    },
    signal: controller.signal
    });
    clearTimeout(timeoutId);
    const json = await res.json();

    const formatado = `${digits.slice(0,3)}.${digits.slice(3,6)}.${digits.slice(6,9)}-${digits.slice(9)}`;
    document.getElementById('cpfDetectado').textContent = formatado;

    if (json.success) {
    document.getElementById('nomeResultado').textContent = `Nome: ${json.data.name}`;
    document.getElementById('nascResultado').textContent = `Nascimento: ${json.data.birthDate}`;
    document.getElementById('dadosApi').style.display = 'block';
    }

    dropZone.style.display = 'none';
    document.getElementById('resultadoContainer').style.display = 'flex';
    } catch (err) {
    clearTimeout(timeoutId);
    exibirErro('Erro ao verificar CPF. Tente novamente.');
    }
}

function exibirErro(mensagem) {
    dropProcessing.style.display = 'none';
    dropIdle.style.display = 'block';
    dropZone.classList.remove('processing');
    dropZone.classList.add('error');
    document.getElementById('processingText').textContent = mensagem;

    setTimeout(() => dropZone.classList.remove('error'), 3000);
}

function resetar() {
    document.getElementById('resultadoContainer').style.display = 'none';
    dropZone.style.display = 'block';
    dropIdle.style.display = 'block';
    dropProcessing.style.display = 'none';
    dropZone.className = 'drop-zone';
    document.getElementById('progressFill').style.width = '0%';
    fileInput.value = '';
}

Acessibilidade na zona de drop

O drag and drop é inerentemente uma interação visual e baseada em mouse. Para garantir acessibilidade, sempre ofereça uma alternativa via teclado — o botão "Selecionar arquivo" cumpre essa função.

<div
    class="drop-zone"
    role="button"
    tabindex="0"
    aria-label="Area para arrastar documento de CPF. Pressione Enter para selecionar um arquivo."
    onkeydown="if(event.key==='Enter') document.getElementById('fileInput').click();"
>

Considerações de segurança

  • O processamento OCR acontece inteiramente no navegador — nenhuma imagem do documento é enviada ao servidor.
  • Após a extração do CPF, descarte a imagem da memória chamando URL.revokeObjectURL.
  • Informe ao usuário que o documento é processado localmente e não será armazenado.

Perguntas frequentes

O que é necessário para implementar validação de CPF com drag and drop?

Para montar o fluxo completo você precisa de três elementos: a zona de drop em HTML/JavaScript tratando os eventos nativos da API de arrastar e soltar do navegador, o Tesseract.js rodando OCR localmente para extrair o número do CPF da imagem, e a chamada à API CPFHub.io para confirmar a situação do CPF junto à Receita Federal. O processamento da imagem fica inteiramente no navegador; apenas o número do CPF é enviado à API.

Quanto tempo leva a validação do CPF após o upload do documento?

O OCR local com Tesseract.js leva de 1 a 3 segundos dependendo da qualidade da imagem. A consulta à API CPFHub.io retorna em cerca de 900ms. O fluxo total — da soltura do arquivo até exibição do nome e situação — costuma concluir em menos de 5 segundos em uma conexão normal.

Como garantir conformidade com a LGPD ao processar imagens de documentos?

Como o OCR roda no navegador, a imagem nunca trafega pelo seu servidor, o que reduz o escopo de dados pessoais tratados. Descarte o objeto de URL com URL.revokeObjectURL após a extração e informe ao usuário na própria interface que o documento é processado localmente. A ANPD orienta que dados de identificação devem ser tratados com base no princípio da necessidade — armazene apenas o número do CPF, não a imagem.

O plano gratuito da CPFHub.io é suficiente para testar esse fluxo?

Sim. O plano gratuito oferece 50 consultas por mês sem cartão de crédito, o que cobre bem a fase de desenvolvimento e testes. Quando o volume aumentar, o plano Pro inclui 1.000 consultas por R$149/mês. Se o limite for ultrapassado em qualquer plano, a API não bloqueia — cobra R$0,15 por consulta adicional.


Conclusão

O drag and drop de documentos para verificação de CPF entrega uma experiência moderna e direta, especialmente em aplicações desktop. Ao combinar upload por arrasto, OCR no navegador e validação via API, o fluxo elimina etapas manuais e reduz erros de digitação — o usuário não precisa copiar o número do documento: a interface encontra e confirma por conta própria.

Do ponto de vista de privacidade, a arquitetura com OCR local é uma vantagem concreta: a imagem do documento nunca sai do dispositivo do usuário, apenas o número do CPF é transmitido. Isso simplifica o compliance com a LGPD e diminui o risco de vazamento de dados sensíveis. Para equipes que precisam escalar esse fluxo com segurança e disponibilidade garantida, a API CPFHub.io oferece a infraestrutura necessária.

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