Busca en todas las páginas de la documentación
🤖 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>
);
}Cuándo usarlo: Necesitas reordenamiento de arrastrar y soltar, listas ordenables, tableros kanban o elementos de interfaz arrastrables con accesibilidad 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>
);
}Lo que esto demuestra:
DragOverlay para una vista previa de arrastre personalizadaonDragOveronDragEndKeyboardSensorDndContext es el proveedor raíz que gestiona el estado de arrastre, sensores y detección de colisionesPointerSensor, KeyboardSensor, TouchSensor) determinan cómo se inician y rastrean las interacciones de arrastreuseSortable combina useDraggable y useDroppable en un solo hook para elementos que son arrastrables y soltablesSortableContext rastrea el orden de los elementos y proporciona la estrategia de ordenación (vertical, horizontal o cuadrícula)closestCenter, closestCorners, rectIntersection) determinan cuál es el objetivo soltable activo durante un arrastreDragOverlay renderiza un elemento flotante que sigue el cursor, separado de la posición DOM del elemento arrastradoarrayMove es una utilidad que reordena un array moviendo un elemento de un índice a otroLista ordenable 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>Asa de arrastre (solo una parte del elemento es arrastrable):
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>
);
}Restricción del eje de arrastre:
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>
{/* ... */}
</DndContext>DragEndEvent, DragStartEvent, DragOverEvent proporcionan objetos active y over tipadosactive.id y over.id son UniqueIdentifier (string o number)as string cuando sabes que tus IDs son stringsuseSortable devuelve un objeto tipado; desestructura solo las propiedades que necesitasimport type { DragEndEvent, UniqueIdentifier } from "@dnd-kit/core";
function handleDragEnd(event: DragEndEvent) {
const activeId: UniqueIdentifier = event.active.id;
const overId: UniqueIdentifier | undefined = event.over?.id;
}Los elementos saltan al empezar a arrastrar - El espacio del elemento arrastrado se colapsa, causando cambio de diseño. Solución: Usa DragOverlay para la vista previa visual y mantén el elemento original en su lugar con opacidad reducida.
Arrastres accidentales al hacer clic - Los clics individuales activan eventos de arrastre. Solución: Añade activationConstraint: { distance: 8 } a PointerSensor para requerir 8px de movimiento antes de activar.
Los IDs deben ser únicos en todos los contenedores - Si dos elementos en diferentes columnas comparten un ID, dnd-kit no puede determinar el contenedor correcto. Solución: Usa IDs globalmente únicos (p. ej., UUIDs) para todos los elementos arrastrables.
Desincronización de hidratación SSR - dnd-kit usa mediciones DOM que no existen en el servidor. Solución: Asegúrate de que el componente esté marcado como "use client" y envuélvelo en un límite de solo cliente si es necesario.
Rendimiento con muchos elementos - Renderizar cientos de elementos ordenables causa retrasos. Solución: Usa virtualización (p. ej., react-virtual) y solo envuelve elementos visibles en useSortable.
| Librería | Lo mejor para | Compensación |
|---|---|---|
| dnd-kit | DnD moderno, accesible y componible | Requiere más configuración para casos complejos |
| react-beautiful-dnd | Listas ordenables simples | Obsoleto, ya no se mantiene |
| react-dnd | Interacciones DnD personalizadas complejas | Curva de aprendizaje más pronunciada, limitaciones del backend HTML5 |
| @hello-pangea/dnd | Reemplazo compatible con react-beautiful-dnd | Fork, mantenido por la comunidad |
| Arrastrar nativo HTML | Arrastre simple sin librerías | Sin accesibilidad de teclado, control limitado |
@dnd-kit/core -- proporciona DndContext, sensores y detección de colisiones@dnd-kit/sortable -- proporciona SortableContext, useSortable y arrayMove@dnd-kit/utilities -- proporciona CSS.Transform.toString para aplicar transformacionesnpm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesDndContext es el proveedor raíz que gestiona todo el estado de arrastrar y soltarDndContextonDragStart, onDragOver y onDragEnduseSortable combina ambos hooks en uno para elementos que son tanto arrastrables como soltablesuseDraggable + useDroppable por separado solo cuando los elementos son arrastrables pero no ordenablesDragOverlay para renderizar una vista previa flotante que siga el cursoropacity: 0.5 mientras arrastrasDragOverlay se renderiza fuera del flujo DOM normal y evita cambios de diseñoconst sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
})
);La restricción distance: 8 requiere 8px de movimiento antes de que se active un arrastre.
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}>Drag handle</button>
<span>{id}</span>
</div>
);
}Aplica attributes y listeners al elemento del asa, no al contenedor.
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 es string | number. Convierte con as string si tus IDs son siempre strings.
"use client"closestCenter -- lo mejor para listas ordenables de un solo contenedorclosestCorners -- lo mejor para configuraciones de múltiples contenedores (tableros kanban)rectIntersection -- se activa cuando los rectángulos se superponen, bueno para diseños de cuadrículaclosestCenter es el valor predeterminado más comúnuseSortable causa retrasos notables@tanstack/react-virtual) para renderizar solo elementos visiblesuseSortableRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥