File Input
ProntoUpload de arquivo com drag-and-drop, preview, validação de tipo/tamanho e progresso de upload.
Usar quando
Anexar PDF/imagem em processo, contrato, mensagem. Importar planilha (CSV). Upload de logo do escritório.
Não usar quando
Edição inline (use Editor de Documentos). Captura de foto da câmera (use input com capture).
Variantes
Estados
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Drag enter | dragenter na zona | Borda dashed → solid + background brassTint | 150ms ease |
| Drop animation | drop válido | Zone scale 0.98 brevemente, depois lista renderiza | 200ms |
| Upload progress | XHR onProgress | Barra cresce de 0% a 100% por arquivo | follows network |
| Success morph | upload completo | Ícone arquivo muda pra ✓ verde + opacity full | 250ms |
| Error inline | tipo/tamanho inválido | Item ganha borda carmim + msg + ícone X | 150ms |
| Remove file | click X num item | Item desliza horizontal + opacity 0 | 200ms |
| Click to browse | click na zona vazia | Abre file picker nativo | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Foco na zona |
| Enter / Space | Abre file picker |
| Tab → X | Foco em remover (lista) |
| Enter no X | Remove arquivo |
ARIA esperado
- role="button" aria-label="Selecionar arquivo"
- aria-describedby="hint" para tipos aceitos
- <input type="file" hidden /> com onChange
- Lista: <ul role="list"><li><button aria-label="Remover X" />
Notas
- Drag-and-drop é nice-to-have — sempre suporte click para abrir picker.
- Validação visual e aria: tipo errado não pode passar silenciosamente.
- Mostre tipos aceitos e tamanho máximo no hint visível.
- Em mobile, drag-and-drop não existe — picker nativo abre com Enter.
Código
'use client';
import { useRef, useState } from 'react';
import { Upload } from 'lucide-react';
export function FileInput({ accept, maxBytes, multiple, onFiles }: Props) {
const ref = useRef<HTMLInputElement>(null);
const [drag, setDrag] = useState(false);
function handleFiles(list: FileList | null) {
if (!list) return;
const files = Array.from(list).filter(f => {
if (accept && !accept.includes(f.type)) return false;
if (maxBytes && f.size > maxBytes) return false;
return true;
});
onFiles(files);
}
return (
<div
onClick={() => ref.current?.click()}
onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
onDragLeave={() => setDrag(false)}
onDrop={(e) => { e.preventDefault(); setDrag(false); handleFiles(e.dataTransfer.files); }}
role="button" tabIndex={0}
style={{
border: `1.5px dashed ${drag ? T.borderInk : T.border}`,
background: drag ? T.brassTint : T.surface,
borderRadius: RADIUS.lg,
padding: SP[8],
textAlign: 'center',
cursor: 'pointer',
}}
>
<Upload size={20} />
<p>Arraste arquivos ou clique</p>
<input ref={ref} type="file" multiple={multiple} accept={accept?.join(',')}
hidden onChange={(e) => handleFiles(e.target.files)} />
</div>
);
}Regras
Faça
- ✓Suporte drag-and-drop + click para picker nativo.
- ✓Mostre tipos aceitos e tamanho máximo visíveis.
- ✓Lista com nome + tamanho + ícone do tipo + remover.
- ✓Progresso por arquivo durante upload.
- ✓Validação imediata: tipo errado/tamanho excedido = erro inline antes do upload.
- ✓Em mobile, picker nativo é suficiente.
Não faça
- ✗Não dependa só de drag-and-drop (não funciona em mobile).
- ✗Não silencie erros (tipo errado deve aparecer claro).
- ✗Não bloqueie UI durante upload — mostre progresso e deixe usuário continuar.
- ✗Não remova arquivo sem confirmação visual (animação ajuda).
- ✗Não permita upload sem feedback de progresso.
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.surface | #FFFFFF | background da zona |
T.border | #E3DFD2 | borda dashed default |
T.borderInk | #000000 | borda quando drag-over |
T.brassTint | rgba(164,124,43,0.08) | background quando drag-over |
T.verde | #2F6B3C | ícone success |
T.carmim | #A32E2E | erro de tipo/tamanho |
RADIUS.lg | 18 | borderRadius da zona |