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:
- Idle — estado padrão, com instruções sobre o que arrastar.
- Drag over — quando o arquivo está sendo arrastado sobre a zona.
- 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.
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.



