Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities"use client";
import { useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
function SortableItem({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="p-3 bg-white border rounded shadow-sm cursor-grab"
>
{id}
</div>
);
}
export default function SortableList() {
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3"]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id as string);
const newIndex = items.indexOf(over.id as string);
return arrayMove(items, oldIndex, newIndex);
});
}
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</div>
</SortableContext>
</DndContext>
);
}Quando usar isso: Você precisa de reordenação de arrastar e soltar, listas ordenáveis, quadros kanban ou elementos de UI arrastáveis com acessibilidade de teclado.
// app/components/KanbanBoard.tsx
"use client";
import { useState } from "react";
import {
DndContext,
DragOverlay,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragStartEvent,
type DragEndEvent,
type DragOverEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useDroppable } from "@dnd-kit/core";
interface Task {
id: string;
title: string;
}
type Columns = Record<string, Task[]>;
function TaskCard({ task }: { task: Task }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
useSortable({ id: task.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="p-3 bg-white border rounded shadow-sm cursor-grab"
>
{task.title}
</div>
);
}
function Column({ id, tasks }: { id: string; tasks: Task[] }) {
const { setNodeRef } = useDroppable({ id });
return (
<div ref={setNodeRef} className="w-72 bg-gray-100 rounded-lg p-4">
<h3 className="font-bold mb-3 capitalize">{id}</h3>
<SortableContext
items={tasks.map((t) => t.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2 min-h-[100px]">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
);
}
export default function KanbanBoard() {
const [columns, setColumns] = useState<Columns>({
todo: [
{ id: "1", title: "Design mockups" },
{ id: "2", title: "Write tests" },
],
"in-progress": [{ id: "3", title: "Build API" }],
done: [{ id: "4", title: "Setup repo" }],
});
const [activeTask, setActiveTask] = useState<Task | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function findColumn(taskId: string): string | undefined {
for (const [columnId, tasks] of Object.entries(columns)) {
if (tasks.some((t) => t.id === taskId)) return columnId;
}
// Check if taskId is a column id (for dropping on empty columns)
if (columns[taskId]) return taskId;
return undefined;
}
function handleDragStart(event: DragStartEvent) {
const col = findColumn(event.active.id as string);
if (col) {
const task = columns[col].find((t) => t.id === event.active.id);
setActiveTask(task ?? null);
}
}
function handleDragOver(event: DragOverEvent) {
const { active, over } = event;
if (!over) return;
const activeCol = findColumn(active.id as string);
const overCol = findColumn(over.id as string);
if (!activeCol || !overCol || activeCol === overCol) return;
setColumns((prev) => {
const activeItems = [...prev[activeCol]];
const overItems = [...prev[overCol]];
const activeIndex = activeItems.findIndex((t) => t.id === active.id);
const [movedTask] = activeItems.splice(activeIndex, 1);
const overIndex = overItems.findIndex((t) => t.id === over.id);
overItems.splice(overIndex >= 0 ? overIndex : overItems.length, 0, movedTask);
return { ...prev, [activeCol]: activeItems, [overCol]: overItems };
});
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
setActiveTask(null);
if (!over) return;
const activeCol = findColumn(active.id as string);
const overCol = findColumn(over.id as string);
if (!activeCol || !overCol || activeCol !== overCol) return;
const items = columns[activeCol];
const oldIndex = items.findIndex((t) => t.id === active.id);
const newIndex = items.findIndex((t) => t.id === over.id);
if (oldIndex !== newIndex) {
setColumns((prev) => ({
...prev,
[activeCol]: arrayMove(prev[activeCol], oldIndex, newIndex),
}));
}
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 p-6">
{Object.entries(columns).map(([id, tasks]) => (
<Column key={id} id={id} tasks={tasks} />
))}
</div>
<DragOverlay>
{activeTask ? (
<div className="p-3 bg-white border rounded shadow-lg rotate-3">
{activeTask.title}
</div>
) : null}
</DragOverlay>
</DndContext>
);
}O que isso demonstra:
DragOverlay para uma prévia de arrastar personalizadaonDragOveronDragEndKeyboardSensorDndContext é o provedor raiz que gerencia o estado de arrastar, sensores e detecção de colisãoPointerSensor, KeyboardSensor, TouchSensor) determinam como as interações de arrastar são iniciadas e rastreadasuseSortable combina useDraggable e useDroppable em um único hook para itens ordenáveisSortableContext rastreia a ordem dos itens e fornece a estratégia de ordenação (vertical, horizontal ou grade)closestCenter, closestCorners, rectIntersection) determinam qual alvo soltável está ativo durante um arrastoDragOverlay renderiza um elemento flutuante que segue o cursor, separado da posição DOM do item arrastadoarrayMove é uma utilidade que reordena um array movendo um elemento de um índice para outroLista ordenável horizontal:
import { horizontalListSortingStrategy } from "@dnd-kit/sortable";
<SortableContext items={items} strategy={horizontalListSortingStrategy}>
<div className="flex gap-2">
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</div>
</SortableContext>Manipulador de arrastar (apenas parte do item é arrastável):
function SortableItemWithHandle({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} className="flex items-center gap-2 p-3 border rounded">
<button {...attributes} {...listeners} className="cursor-grab">
☰
</button>
<span>{id}</span>
</div>
);
}Restringindo o eixo de arrastar:
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>
{/* ... */}
</DndContext>DragEndEvent, DragStartEvent, DragOverEvent fornecem objetos active e over tipadosactive.id e over.id são UniqueIdentifier (string ou number)as string quando seus IDs forem garantidamente stringsuseSortable retorna um objeto tipado; desestruture apenas as propriedades que você precisaimport type { DragEndEvent, UniqueIdentifier } from "@dnd-kit/core";
function handleDragEnd(event: DragEndEvent) {
const activeId: UniqueIdentifier = event.active.id;
const overId: UniqueIdentifier | undefined = event.over?.id;
}Itens pulam ao iniciar o arrasto - O espaço do item arrastado colapsa, causando deslocamento de layout. Correção: Use DragOverlay para a prévia visual e mantenha o item original no lugar com opacidade reduzida.
Arrastos acidentais ao clicar - Cliques únicos acionam eventos de arrastar. Correção: Adicione activationConstraint: { distance: 8 } ao PointerSensor para exigir 8px de movimento antes de ativar.
IDs devem ser únicos em todos os contêineres - Se dois itens em colunas diferentes compartilharem um ID, o dnd-kit não conseguirá determinar o contêiner correto. Correção: Use IDs globalmente únicos (por exemplo, UUIDs) para todos os itens arrastáveis.
Diferença de hidratação SSR - O dnd-kit usa medições DOM que não existem no servidor. Correção: Certifique-se de que o componente esteja marcado como "use client" e envolva-o em um limite apenas para cliente, se necessário.
Desempenho com muitos itens - Renderizar centenas de itens ordenáveis causa lentidão. Correção: Use virtualização (por exemplo, react-virtual) e envolva apenas os itens visíveis com useSortable.
| Biblioteca | Melhor Para | Contrapartida |
|---|---|---|
| dnd-kit | DnD moderno, acessível e componível | Requer mais configuração para casos complexos |
| react-beautiful-dnd | Listas ordenáveis simples | Descontinuado, não mais mantido |
| react-dnd | Interações DnD personalizadas complexas | Curva de aprendizado mais acentuada, limitações do backend HTML5 |
| @hello-pangea/dnd | Substituição direta do react-beautiful-dnd | Fork, mantido pela comunidade |
| HTML drag nativo | Arrastar simples sem bibliotecas | Sem acessibilidade de teclado, controle limitado |
@dnd-kit/core -- fornece DndContext, sensores e detecção de colisão@dnd-kit/sortable -- fornece SortableContext, useSortable e arrayMove@dnd-kit/utilities -- fornece CSS.Transform.toString para aplicar transformaçõesnpm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesDndContext é o provedor raiz que gerencia todo o estado de arrastar e soltarDndContextonDragStart, onDragOver e onDragEnduseSortable combina ambos os hooks em um para itens que são tanto arrastáveis quanto soltáveisuseDraggable + useDroppable separadamente apenas quando os itens forem arrastáveis, mas não ordenáveisDragOverlay para renderizar uma prévia flutuante que segue o cursoropacity: 0.5 durante o arrastoDragOverlay renderiza fora do fluxo normal do DOM e evita deslocamentos de layoutconst sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
})
);A restrição distance: 8 exige 8px de movimento antes que um arrasto seja ativado.
function SortableItem({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
return (
<div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}>
<button {...attributes} {...listeners}>Manipulador de arrastar</button>
<span>{id}</span>
</div>
);
}Aplique attributes e listeners ao elemento manipulador, não ao contêiner.
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>
{/* ... */}
</DndContext>import type { DragEndEvent, UniqueIdentifier } from "@dnd-kit/core";
function handleDragEnd(event: DragEndEvent) {
const activeId: UniqueIdentifier = event.active.id;
const overId: UniqueIdentifier | undefined = event.over?.id;
}UniqueIdentifier é string | number. Use as string se seus IDs forem sempre strings.
"use client"closestCenter -- melhor para listas ordenáveis de contêiner únicoclosestCorners -- melhor para configurações de múltiplos contêineres (quadros kanban)rectIntersection -- dispara quando retângulos se sobrepõem, bom para layouts de gradeclosestCenter é o padrão mais comumuseSortable causa lentidão perceptível@tanstack/react-virtual) para renderizar apenas os itens visíveisuseSortableRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥