src/skills/web-dnd-dnd-kit/SKILL.md
Drag and drop with @dnd-kit - draggable, droppable, sortable, collision detection, sensors, accessibility
npx skillsauth add agents-inc/skills web-dnd-dnd-kitInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use
@dnd-kit/corefor basic drag/drop (useDraggable,useDroppable,DndContext). Use@dnd-kit/sortablefor sortable lists (useSortable,SortableContext,arrayMove). UseDragOverlayfor cross-container drag, scrollable containers, and smooth drop animations. Configure sensors for pointer/touch/keyboard input with activation constraints. Always provide keyboard and screen reader accessibility viaKeyboardSensorand customannouncements.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST wrap all drag-and-drop content in a <DndContext> provider -- hooks only work inside DndContext)
(You MUST use DragOverlay when items move between containers or live in scrollable containers -- transform alone breaks in these cases)
(You MUST configure KeyboardSensor with sortableKeyboardCoordinates for sortable lists -- keyboard users cannot reorder without it)
(You MUST keep DragOverlay always mounted and conditionally render its children -- unmounting DragOverlay breaks drop animations)
(You MUST use named constants for all activation constraints, distances, and timing values -- NO magic numbers)
</critical_requirements>
Auto-detection: @dnd-kit, dnd-kit, DndContext, useDraggable, useDroppable, useSortable, SortableContext, DragOverlay, useSensors, useSensor, PointerSensor, KeyboardSensor, closestCenter, closestCorners, rectIntersection, pointerWithin, arrayMove, sortableKeyboardCoordinates, CSS.Transform, @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, @dnd-kit/modifiers
When to use:
When NOT to use:
Key patterns covered:
Detailed Resources:
@dnd-kit is a modular, lightweight drag-and-drop toolkit for React built around hooks. It separates concerns into focused packages: @dnd-kit/core for the drag/drop primitives, @dnd-kit/sortable for list reordering, @dnd-kit/utilities for CSS transform helpers, and @dnd-kit/modifiers for movement constraints.
Core principles:
useDraggable, useDroppable, and useSortable keep drag logic colocated with componentsPackage overview:
| Package | Purpose |
| -------------------- | ------------------------------------------------------------------------------------------------ |
| @dnd-kit/core | DndContext, useDraggable, useDroppable, DragOverlay, sensors, collision detection |
| @dnd-kit/sortable | SortableContext, useSortable, sorting strategies, arrayMove, sortableKeyboardCoordinates |
| @dnd-kit/utilities | CSS.Transform.toString, CSS.Transition.toString |
| @dnd-kit/modifiers | restrictToVerticalAxis, restrictToHorizontalAxis, restrictToParentElement, restrictToWindowEdges |
DndContext is the provider that connects draggable and droppable elements. useDraggable makes an element draggable. useDroppable makes an element a drop target.
import { DndContext, type DragEndEvent } from "@dnd-kit/core";
function App() {
const [parent, setParent] = useState<string | null>(null);
function handleDragEnd(event: DragEndEvent) {
const { over } = event;
setParent(over ? String(over.id) : null);
}
return (
<DndContext onDragEnd={handleDragEnd}>
<DraggableItem id="item-1" />
<DroppableZone id="zone-a">
{parent === "zone-a" && <span>Dropped here</span>}
</DroppableZone>
</DndContext>
);
}
Why good: DndContext wraps all participants, event handler updates state on drop, draggable and droppable use unique string IDs
See examples/core.md Pattern 1 for full useDraggable and useDroppable implementations with TypeScript types.
SortableContext + useSortable provides reorderable lists. Use arrayMove from @dnd-kit/sortable to update state on drag end.
import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core";
import {
SortableContext,
arrayMove,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
function SortableList() {
const [items, setItems] = useState(["a", "b", "c", "d"]);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((prev) => {
const oldIndex = prev.indexOf(String(active.id));
const newIndex = prev.indexOf(String(over.id));
return arrayMove(prev, oldIndex, newIndex);
});
}
}
return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</SortableContext>
</DndContext>
);
}
Why good: closestCenter is forgiving for vertical lists, verticalListSortingStrategy optimizes transform calculations, arrayMove produces a new array (immutable)
See examples/core.md Pattern 2 for the full SortableItem component using useSortable and CSS.Transform.
Use DragOverlay instead of transforming the dragged element directly when items move between containers, live in scrollable/virtualized containers, or need custom drag previews.
import {
DndContext,
DragOverlay,
type DragStartEvent,
type DragEndEvent,
} from "@dnd-kit/core";
function Board() {
const [activeId, setActiveId] = useState<string | null>(null);
return (
<DndContext
onDragStart={(event: DragStartEvent) =>
setActiveId(String(event.active.id))
}
onDragEnd={(event: DragEndEvent) => {
handleDragEnd(event);
setActiveId(null);
}}
>
{/* containers and sortable items */}
<DragOverlay>
{activeId ? <ItemPreview id={activeId} /> : null}
</DragOverlay>
</DndContext>
);
}
Key rules: Keep DragOverlay always mounted (conditionally render children, not the component). Children rendered inside DragOverlay must NOT use useDraggable. Default drop animation is 250ms ease -- disable with dropAnimation={null}.
See examples/advanced.md Pattern 1 for DragOverlay with sortable lists and custom drop animations.
Sensors control which input methods activate dragging. Use useSensors to compose multiple sensors with activation constraints.
import {
useSensor,
useSensors,
PointerSensor,
KeyboardSensor,
} from "@dnd-kit/core";
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
const ACTIVATION_DISTANCE_PX = 8;
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: ACTIVATION_DISTANCE_PX },
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
<DndContext sensors={sensors}>{/* ... */}</DndContext>;
Why good: distance constraint prevents accidental drags on click, KeyboardSensor with sortableKeyboardCoordinates enables arrow-key reordering, named constant for distance threshold
Sensor types: PointerSensor (unified pointer events), MouseSensor (mouse only), TouchSensor (touch with delay support), KeyboardSensor (arrow keys + Space/Enter)
See examples/core.md Pattern 3 for all sensor configurations including touch delay and tolerance.
Choose the collision algorithm based on your layout.
| Algorithm | Import | Best for |
| ------------------ | --------------- | ------------------------------------------------ |
| rectIntersection | @dnd-kit/core | General drop zones (default) |
| closestCenter | @dnd-kit/core | Sortable lists -- forgiving, no overlap required |
| closestCorners | @dnd-kit/core | Stacked/overlapping containers (Kanban columns) |
| pointerWithin | @dnd-kit/core | Precision drop -- pointer must be inside target |
Gotcha: pointerWithin only works with pointer-based sensors. Compose it with a fallback for keyboard support.
See examples/core.md Pattern 4 for collision detection selection and custom composition.
Choose the strategy based on list orientation.
| Strategy | Import | Use case |
| ------------------------------- | ------------------- | --------------------------------------------- |
| rectSortingStrategy | @dnd-kit/sortable | Grids (default, does NOT support virtualized) |
| verticalListSortingStrategy | @dnd-kit/sortable | Vertical lists (supports virtualized) |
| horizontalListSortingStrategy | @dnd-kit/sortable | Horizontal lists (supports virtualized) |
| rectSwappingStrategy | @dnd-kit/sortable | Swap mode (items trade positions) |
Always match the strategy to your layout -- using rectSortingStrategy on a vertical list produces suboptimal animations.
@dnd-kit provides built-in accessibility. useDraggable applies role="button", aria-roledescription="draggable", tabindex="0", and links to screen reader instructions via aria-describedby.
Customize announcements via the announcements prop on DndContext:
const announcements = {
onDragStart({ active }: { active: { id: UniqueIdentifier } }) {
return `Picked up item ${active.id}`;
},
onDragOver({
active,
over,
}: {
active: { id: UniqueIdentifier };
over: { id: UniqueIdentifier } | null;
}) {
if (over) return `Item ${active.id} moved over ${over.id}`;
return `Item ${active.id} is no longer over a drop target`;
},
onDragEnd({
active,
over,
}: {
active: { id: UniqueIdentifier };
over: { id: UniqueIdentifier } | null;
}) {
if (over) return `Item ${active.id} dropped on ${over.id}`;
return `Item ${active.id} was dropped`;
},
onDragCancel({ active }: { active: { id: UniqueIdentifier } }) {
return `Dragging cancelled. Item ${active.id} was dropped`;
},
};
<DndContext announcements={announcements}>{/* ... */}</DndContext>;
Why good: Screen readers announce drag state changes in real time, position-based messages ("position 2 of 5") are more useful than generic "moved over" messages
See examples/core.md Pattern 5 for position-based announcements and custom screen reader instructions.
For Kanban boards, each column has its own SortableContext. Items move between containers via onDragOver (update state as item crosses boundaries) and onDragEnd (finalize position).
Key decisions for multi-container:
closestCorners collision detection (handles stacked columns better than closestCenter)DragOverlay (items unmount from source container during cross-container drag)activeId to render the drag preview in the overlayonDragOver for real-time container transfers, onDragEnd for final placementSee examples/advanced.md Pattern 2 for the complete Kanban implementation.
Modifiers constrain drag movement. Apply to DndContext (affects all dragging) or DragOverlay (affects overlay only).
import {
restrictToVerticalAxis,
restrictToParentElement,
} from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>{/* ... */}</DndContext>;
| Modifier | Package | Effect |
| -------------------------- | -------------------- | --------------------------------- |
| restrictToVerticalAxis | @dnd-kit/modifiers | Lock movement to Y axis |
| restrictToHorizontalAxis | @dnd-kit/modifiers | Lock movement to X axis |
| restrictToParentElement | @dnd-kit/modifiers | Constrain to parent bounds |
| restrictToWindowEdges | @dnd-kit/modifiers | Prevent dragging outside viewport |
Different modifiers can be applied to DndContext and DragOverlay independently.
</patterns><decision_framework>
Do you need sortable lists?
|-- YES -> @dnd-kit/core + @dnd-kit/sortable (+ @dnd-kit/utilities for CSS.Transform)
+-- NO -> Just drag/drop zones?
+-- YES -> @dnd-kit/core only
Do items move between containers?
|-- YES -> Use DragOverlay (items unmount from source during drag)
+-- NO -> Is the draggable inside a scrollable/virtualized container?
|-- YES -> Use DragOverlay (avoids overflow clipping)
+-- NO -> Do you need a custom drag preview different from the source?
|-- YES -> Use DragOverlay
+-- NO -> Transform approach is sufficient (simpler)
Single sortable list?
|-- YES -> closestCenter (forgiving, no overlap needed)
+-- NO -> Stacked containers (Kanban columns)?
|-- YES -> closestCorners (better for overlapping droppables)
+-- NO -> Precision drop targets (trash bin, category bins)?
|-- YES -> pointerWithin (only triggers when pointer is inside)
+-- NO -> rectIntersection (default, general purpose)
</decision_framework>
<red_flags>
High Priority Issues:
DndContext wrapper -- useDraggable/useDroppable/useSortable fail silently without itDragOverlay -- breaks drop animations; always mount it, conditionally render childrenKeyboardSensor -- keyboard users cannot interact with drag-and-drop at allclosestCenter for stacked containers (Kanban) -- often selects the column instead of items within; use closestCornerssortableKeyboardCoordinates on KeyboardSensor for sortable lists -- arrow keys move by pixels instead of to next itemarrayMove returns a new array; do not use .splice() directly on stateMedium Priority Issues:
rectSortingStrategy (default) for vertical-only lists -- verticalListSortingStrategy is more performant and supports virtualizationannouncements -- default messages use IDs which are meaningless to screen reader usersuseDraggable inside DragOverlay children -- the overlay renders a preview, not an interactive draggableGotchas & Edge Cases:
useDraggable and useDroppable can share the same id (they use separate stores), but useSortable combines both so its id must be unique across draggables AND droppablesSortableContext items prop must match the order of rendered children -- mismatches cause animation glitchespointerWithin only works with pointer-based sensors -- compose with closestCenter fallback for keyboard supportCSS.Transform.toString() returns undefined when transform is null -- safe to pass directly to style.transformscaleX/scaleY -- if you don't want scaling, destructure and only use x/y with CSS.Translate.toString()DragOverlay is NOT rendered in a portal by default -- use createPortal if you need it to escape overflow/stacking contextsdata argument on useDraggable/useDroppable is available in event handlers via active.data.current and over.data.current -- useful for carrying metadata (type, container ID)arrayMove is a pure utility -- it does not update state; you must call your setter with its return valuescreenReaderInstructions prop for localization</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST wrap all drag-and-drop content in a <DndContext> provider -- hooks only work inside DndContext)
(You MUST use DragOverlay when items move between containers or live in scrollable containers -- transform alone breaks in these cases)
(You MUST configure KeyboardSensor with sortableKeyboardCoordinates for sortable lists -- keyboard users cannot reorder without it)
(You MUST keep DragOverlay always mounted and conditionally render its children -- unmounting DragOverlay breaks drop animations)
(You MUST use named constants for all activation constraints, distances, and timing values -- NO magic numbers)
Failure to follow these rules will break drag interactions, keyboard accessibility, and drop animations.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events