Eventos de Drag & Drop
Implemente interações nativas de arrastar e soltar do HTML5 com o sistema de eventos sintéticos do React.
Busque em todas as páginas da documentação
Implemente interações nativas de arrastar e soltar do HTML5 com o sistema de eventos sintéticos do React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Evento | Dispara Quando | Dispara Em | Borbulha | Cancelável |
|---|---|---|---|---|
onDragStart | Usuário começa a arrastar um elemento | Elemento arrastado | Sim | Sim |
onDrag | Continuamente enquanto o elemento está sendo arrastado | Elemento arrastado | Sim | Sim |
onDragEnd | Operação de arrastar termina (soltar ou cancelar) | Elemento arrastado | Sim | Não |
onDragEnter | Elemento arrastado entra em um alvo de soltura válido | Alvo de soltura | Sim | Sim |
onDragOver | Elemento arrastado está sobre um alvo de soltura válido (dispara repetidamente) | Alvo de soltura | Sim | Sim |
onDragLeave | Elemento arrastado sai de um alvo de soltura | Alvo de soltura | Sim | Não |
onDrop | Elemento é solto em um alvo de soltura válido | Alvo de soltura | Sim | Sim |
Você deve chamar
e.preventDefault()emonDragOverpara tornar um elemento um alvo de soltura válido. Sem isso,onDropnunca disparará.
Cartão de receita de referência rápida - pronto para copiar e colar.
// Zona básica de soltura de arquivos
function FileDropZone() {
const [over, setOver] = useState(false);
return (
<div
onDragOver={(e) => {
e.preventDefault(); // Necessário para permitir a soltura
setOver(true);
}}
onDragLeave={() => setOver(false)}
onDrop={(e) => {
e.preventDefault();
setOver(false);
const files = Array.from(e.dataTransfer.files);
console.log("Arquivos soltos:", files);
}}
className={over ? "border-blue-500 bg-blue-50" : "border-gray-300"}
style={{ border: "2px dashed", padding: "2rem", textAlign: "center" }}
>
Solte arquivos aqui
</div>
);
}Quando usar isso: Você precisa de upload de arquivos via arrastar e soltar, listas ordenáveis, cartões arrastáveis entre colunas, ou qualquer interação visual de arrastar usando APIs nativas do navegador.
// components/SortableList.tsx
"use client";
import { useState, useRef } from "react";
type Item = { id: string; label: string };
const INITIAL_ITEMS: Item[] = [
{ id: "1", label: "Aprender fundamentos do React" },
{ id: "2", label: "Construir um projeto paralelo" },
{ id: "3", label: "Escrever testes" },
{ id: "4", label: "Implantar em produção" },
{ id: "5", label: "Monitorar e iterar" },
];
export default function SortableList() {
const [items, setItems] = useState<Item[]>(INITIAL_ITEMS);
const dragItem = useRef<number | null>(null);
const dragOverItem = useRef<number | null>(null);
const [dragIndex, setDragIndex] = useState<number | null>(null);
const handleDragStart = (e: React.DragEvent<HTMLLIElement>, index: number) => {
dragItem.current = index;
setDragIndex(index);
// Definir dados de arrastar (necessário para Firefox)
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", String(index));
// Tornar o fantasma de arrastar semi-transparente
if (e.currentTarget instanceof HTMLElement) {
e.currentTarget.style.opacity = "0.4";
}
};
const handleDragOver = (e: React.DragEvent<HTMLLIElement>, index: number) => {
e.preventDefault(); // Necessário para permitir a soltura
e.dataTransfer.dropEffect = "move";
dragOverItem.current = index;
};
const handleDragEnd = (e: React.DragEvent<HTMLLIElement>) => {
// Resetar opacidade
if (e.currentTarget instanceof HTMLElement) {
e.currentTarget.style.opacity = "1";
}
if (dragItem.current === null || dragOverItem.current === null) {
setDragIndex(null);
return;
}
// Reordenar
const newItems = [...items];
const [removed] = newItems.splice(dragItem.current, 1);
newItems.splice(dragOverItem.current, 0, removed);
setItems(newItems);
dragItem.current = null;
dragOverItem.current = null;
setDragIndex(null);
};
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-xl font-bold mb-4">Arraste para reordenar</h2>
<ul className="space-y-2">
{items.map((item, index) => (
<li
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
className={`
flex items-center gap-3 px-4 py-3 bg-white border rounded shadow-sm
cursor-grab active:cursor-grabbing
${dragIndex === index ? "opacity-40" : "opacity-100"}
hover:shadow-md transition-shadow
`}
>
<span className="text-gray-400 select-none">:::</span>
<span>{item.label}</span>
</li>
))}
</ul>
</div>
);
}O que isso demonstra:
draggableeffectAllowed e dropEffect para feedback correto do cursordraggable={true} no elemento de origem.dragstart -> drag (repetido) -> dragenter / dragover (nos alvos) -> drop ou dragend.onDragOver chama e.preventDefault(). Sem isso, o navegador ignora a soltura.e.dataTransfer, que suporta múltiplos tipos MIME simultaneamente.dragover e dragenter, você só pode ler dataTransfer.types -- os dados reais de getData() só estão disponíveis em drop e dragstart por razões de segurança.Zona de soltura de arquivos com validação:
function ValidatedDropZone() {
const [status, setStatus] = useState<"idle" | "over" | "error">("idle");
const [files, setFiles] = useState<File[]>([]);
const ALLOWED_TYPES = ["image/png", "image/jpeg", "application/pdf"];
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
return (
<div
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
setStatus("over");
}}
onDragLeave={() => setStatus("idle")}
onDrop={(e) => {
e.preventDefault();
const dropped = Array.from(e.dataTransfer.files);
const valid = dropped.filter(
(f) => ALLOWED_TYPES.includes(f.type) && f.size <= MAX_SIZE
);
if (valid.length < dropped.length) {
setStatus("error");
setTimeout(() => setStatus("idle"), 2000);
} else {
setStatus("idle");
}
setFiles((prev) => [...prev, ...valid]);
}}
className={`border-2 border-dashed rounded p-8 text-center transition-colors ${
status === "over"
? "border-blue-500 bg-blue-50"
: status === "error"
? "border-red-500 bg-red-50"
: "border-gray-300"
}`}
>
<p>Solte arquivos PNG, JPEG ou PDF (máximo 5 MB)</p>
{files.length > 0 && (
<ul className="mt-4 text-sm text-left">
{files.map((f, i) => (
<li key={i}>{f.name} ({(f.size / 1024).toFixed(1)} KB)</li>
))}
</ul>
)}
</div>
);
}Prévia de arrastar personalizada / imagem fantasma:
function CustomGhostDrag() {
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
// Criar uma imagem fantasma personalizada
const ghost = document.createElement("div");
ghost.textContent = "Movendo...";
ghost.style.cssText =
"padding: 8px 16px; background: #3b82f6; color: white; border-radius: 4px; position: absolute; top: -1000px;";
document.body.appendChild(ghost);
e.dataTransfer.setDragImage(ghost, 0, 0);
// Limpar após o navegador capturar a imagem fantasma
requestAnimationFrame(() => document.body.removeChild(ghost));
};
return (
<div draggable onDragStart={handleDragStart} className="p-4 border rounded cursor-grab">
Arraste-me (fantasma personalizado)
</div>
);
}Cartões arrastáveis entre contêineres:
function KanbanBoard() {
const [columns, setColumns] = useState<Record<string, string[]>>({
todo: ["Tarefa A", "Tarefa B"],
doing: ["Tarefa C"],
done: ["Tarefa D"],
});
const handleDrop = (e: React.DragEvent, targetColumn: string) => {
e.preventDefault();
const data = e.dataTransfer.getData("application/json");
const { sourceColumn, task } = JSON.parse(data);
if (sourceColumn === targetColumn) return;
setColumns((prev) => ({
...prev,
[sourceColumn]: prev[sourceColumn].filter((t) => t !== task),
[targetColumn]: [...prev[targetColumn], task],
}));
};
return (
<div className="flex gap-4">
{Object.entries(columns).map(([colName, tasks]) => (
<div
key={colName}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => handleDrop(e, colName)}
className="w-48 p-4 bg-gray-100 rounded min-h-[200px]"
>
<h3 className="font-bold mb-2 capitalize">{colName}</h3>
{tasks.map((task) => (
<div
key={task}
draggable
onDragStart={(e) => {
e.dataTransfer.setData(
"application/json",
JSON.stringify({ sourceColumn: colName, task })
);
e.dataTransfer.effectAllowed = "move";
}}
className="p-2 mb-2 bg-white rounded shadow cursor-grab"
>
{task}
</div>
))}
</div>
))}
</div>
);
}Arrastar com múltiplos formatos de dados:
function MultiFormatDrag() {
const handleDragStart = (e: React.DragEvent) => {
// Definir múltiplos formatos para que diferentes alvos de soltura possam escolher o que precisam
e.dataTransfer.setData("text/plain", "Olá, mundo!");
e.dataTransfer.setData("text/html", "<strong>Olá, mundo!</strong>");
e.dataTransfer.setData(
"application/json",
JSON.stringify({ message: "Olá", timestamp: Date.now() })
);
};
return (
<div draggable onDragStart={handleDragStart} className="p-4 border rounded cursor-grab">
Arraste-me -- levo texto, HTML e JSON
</div>
);
}Arrastar entre contêineres com indicador visual de inserção:
function DragWithIndicator() {
const [insertIndex, setInsertIndex] = useState<number | null>(null);
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3"]);
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
// Mostrar indicador acima ou abaixo com base na posição do cursor
setInsertIndex(e.clientY < midY ? index : index + 1);
};
return (
<ul onDragLeave={() => setInsertIndex(null)}>
{items.map((item, i) => (
<li key={item}>
{insertIndex === i && (
<div className="h-0.5 bg-blue-500 mx-2" />
)}
<div
draggable
onDragOver={(e) => handleDragOver(e, i)}
className="p-3 border-b"
>
{item}
</div>
</li>
))}
{insertIndex === items.length && (
<div className="h-0.5 bg-blue-500 mx-2" />
)}
</ul>
);
}// O genérico DragEvent especifica o elemento ao qual o manipulador está anexado
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.currentTarget; // HTMLDivElement
e.dataTransfer; // DataTransfer (sempre disponível em eventos de arrastar)
};
// Propriedades e métodos chave do DataTransfer
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.files; // FileList -- arquivos soltos do sistema operacional
e.dataTransfer.items; // DataTransferItemList -- todos os itens
e.dataTransfer.types; // readonly string[] -- tipos MIME disponíveis
e.dataTransfer.dropEffect; // "none" | "copy" | "link" | "move"
e.dataTransfer.effectAllowed; // "none" | "copy" | "copyLink" | "copyMove" | "link" | "linkMove" | "move" | "all" | "uninitialized"
// Lendo dados
const text: string = e.dataTransfer.getData("text/plain");
const json: string = e.dataTransfer.getData("application/json");
// Iteração de arquivos tipada
const files: File[] = Array.from(e.dataTransfer.files);
files.forEach((file: File) => {
file.name; // string
file.type; // string (MIME)
file.size; // number (bytes)
});
};
// Tipo de manipulador de atalho
const onDrag: React.DragEventHandler<HTMLLIElement> = (e) => {
// e é React.DragEvent<HTMLLIElement>
};
// Tipagem de dados de arrastar com um helper
type DragPayload = { sourceColumn: string; taskId: string };
function setDragPayload(e: React.DragEvent, payload: DragPayload) {
e.dataTransfer.setData("application/json", JSON.stringify(payload));
}
function getDragPayload(e: React.DragEvent): DragPayload {
return JSON.parse(e.dataTransfer.getData("application/json"));
}onDrop nunca dispara sem e.preventDefault() em onDragOver -- O comportamento padrão do navegador é rejeitar solturas. Você deve chamar e.preventDefault() em onDragOver em cada elemento que deve aceitar solturas. Correção: Sempre adicione onDragOver={(e) => e.preventDefault()} aos alvos de soltura.
dataTransfer.getData() retorna uma string vazia durante dragover -- Por razões de segurança, os navegadores só permitem a leitura de dados de arrastar nos eventos dragstart e drop. Durante dragover e dragenter, apenas dataTransfer.types está disponível. Correção: Use dataTransfer.types.includes("application/json") para verificar quais dados estão disponíveis durante dragover, e leia os dados reais em onDrop.
Firefox requer dataTransfer.setData() em dragstart -- O Firefox não iniciará uma operação de arrastar a menos que você chame setData com pelo menos um formato. Correção: Sempre chame e.dataTransfer.setData("text/plain", "") no mínimo em seu manipulador onDragStart.
onDragLeave dispara ao entrar em elementos filhos -- Mover o cursor de uma zona de soltura para um elemento filho dentro dela aciona dragleave no pai, seguido por dragenter no filho. Isso causa cintilação ao alternar estados visuais de soltura. Correção: Use um contador (incrementa em dragenter, decrementa em dragleave, reseta em drop) ou verifique e.currentTarget.contains(e.relatedTarget as Node).
A imagem fantasma de arrastar captura o elemento no momento do dragstart -- Quaisquer alterações de estilo feitas durante onDragStart (como adicionar uma classe) podem não aparecer na imagem fantasma porque o navegador captura o instantâneo imediatamente. Correção: Use e.dataTransfer.setDragImage() para imagens fantasmas personalizadas, ou aplique estilos via requestAnimationFrame para garantir que eles entrem em vigor antes da captura.
draggable em elementos com texto torna a seleção de texto impossível -- Definir draggable={true} em um elemento impede que os usuários selecionem texto dentro dele com clique e arrastar. Correção: Defina draggable apenas em alças de arrastar específicas em vez da área de conteúdo inteira, ou alterne draggable com base no contexto da interação.
Navegadores móveis têm suporte nativo de arrastar e soltar pobre ou inexistente -- O Arrastar e Soltar do HTML5 não funciona na maioria dos navegadores móveis (iOS Safari, Chrome Android). Correção: Use eventos de ponteiro ou eventos de toque para interações de arrastar em dispositivos móveis, ou use uma biblioteca como dnd-kit ou @hello-pangea/dnd que lida com dispositivos de toque.
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
dnd-kit | Você precisa de arrastar e soltar acessível e amigável ao toque com listas ordenáveis | Uma simples zona de soltura de arquivos é tudo o que você precisa |
@hello-pangea/dnd (fork de react-beautiful-dnd) | Você precisa de arrastar e soltar animado suave com uma API comprovada | Você precisa arrastar entre iframes ou janelas externas |
Eventos de Ponteiro (onPointerDown/Move/Up) | Você precisa de comportamento de arrastar personalizado não limitado pelas limitações do HTML5 DnD | Soltura de arquivo nativa ou arrastar entre janelas é necessário |
<input type="file"> | Você só precisa de seleção de arquivo, não de uma zona de soltura visual | A UX de arrastar e soltar é um requisito principal |
touch-action do CSS + eventos de ponteiro | Você precisa de interações de arrastar amigáveis para dispositivos móveis | DnD nativo apenas para desktop é suficiente |
Você deve chamar e.preventDefault() no manipulador onDragOver no alvo de soltura. O comportamento padrão do navegador é rejeitar solturas. Sem onDragOver={(e) => e.preventDefault()}, o evento onDrop nunca disparará.
dragstart dispara no elemento arrastado quando o usuário começa a arrastardrag dispara continuamente no elemento arrastado enquanto arrastadragenter / dragover disparam nos alvos de soltura conforme o elemento arrastado se move sobre elesdrop dispara no alvo de soltura quando o usuário soltadragend dispara no elemento arrastado após a operação terminarAdicione o atributo draggable ao elemento: <div draggable>Arraste-me</div>. Em seguida, anexe um manipulador onDragStart para definir os dados de arrastar via e.dataTransfer.setData().
Mover de uma zona de soltura pai para um filho aciona dragleave no pai seguido por dragenter no filho, causando cintilação visual. Corrija isso usando um contador (incrementa em dragenter, decrementa em dragleave, reseta em drop) ou verificando e.currentTarget.contains(e.relatedTarget as Node).
Por segurança, os navegadores só permitem a leitura de dados de arrastar nos eventos dragstart e drop. Durante dragover e dragenter, apenas dataTransfer.types está disponível. Use dataTransfer.types.includes("application/json") para verificar a disponibilidade de dados durante dragover.
// Em onDragStart
e.dataTransfer.setData(
"application/json",
JSON.stringify({ sourceColumn: "todo", taskId: "123" })
);
// Em onDrop
const data = JSON.parse(e.dataTransfer.getData("application/json"));O Firefox requer pelo menos uma chamada para e.dataTransfer.setData() em onDragStart ou ele não iniciará o arrastar. Sempre chame e.dataTransfer.setData("text/plain", "") no mínimo em seu manipulador onDragStart.
const ALLOWED = ["image/png", "image/jpeg"];
const MAX = 5 * 1024 * 1024;
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
const valid = files.filter(
(f) => ALLOWED.includes(f.type) && f.size <= MAX
);
};effectAllowed é definido na origem do arrastar em onDragStart para declarar operações permitidas ("move", "copy", "link", "all")dropEffect é definido no alvo da soltura em onDragOver para indicar a operação atual e controlar o ícone do cursorO atributo draggable intercepta gestos de clique e arrastar que normalmente selecionariam texto. Corrija isso definindo draggable apenas em um elemento de alça de arrastar específico em vez da área de conteúdo inteira.
Não. A maioria dos navegadores móveis (iOS Safari, Chrome Android) tem suporte nativo de arrastar e soltar pobre ou inexistente. Use eventos de ponteiro/toque ou uma biblioteca como dnd-kit ou @hello-pangea/dnd para interações de arrastar amigáveis para dispositivos móveis.
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.files; // FileList
e.dataTransfer.types; // readonly string[]
};
// Helper de payload tipado
type Payload = { sourceColumn: string; taskId: string };
function getDragPayload(e: React.DragEvent): Payload {
return JSON.parse(e.dataTransfer.getData("application/json"));
}const onDrag: React.DragEventHandler<HTMLLIElement> = (e) => {
// e é React.DragEvent<HTMLLIElement>
e.currentTarget; // HTMLLIElement
e.dataTransfer; // DataTransfer
};Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥