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

**Publicado:** 08/03/2026
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/como-implementar-drag-and-drop-de-documento-de-cpf-para-upload-e-validacao

---


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](https://www.w3.org/WAI/WCAG21/Understanding/), 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

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

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

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

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

### Leia também

- [Como implementar validação de CPF em formulários de checkout sem interromper o fluxo](https://cpfhub.io/blog/como-implementar-validacao-cpf-formularios-checkout-sem-interromper-fluxo)
- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)
- [Feedback visual em tempo real na consulta de CPF](https://cpfhub.io/blog/feedback-visual-tempo-real-consulta-cpf)
- [Acessibilidade em formulários com CPF e inclusão digital](https://cpfhub.io/blog/acessibilidade-formularios-cpf-inclusao-digital)

---

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

