Appearance
useDragAndDrop
What this solves
Drag and drop is common in admin UIs:
- Reordering items in a list (tasks, menu items, priorities)
- Moving items between lists (kanban columns, category assignment)
- Sortable tables or grids
Ad-hoc implementations often have:
- Inconsistent drag feedback and drop zones
- Complex state management for active/over items
- Duplicated reorder logic across features
useDragAndDropList and useDragAndDropMultiList provide consistent drag state management built on @dnd-kit.
When to use it
Use these hooks when you need:
useDragAndDropList— Single list reordering (sortable list, priority queue)useDragAndDropMultiList— Multi-list with cross-list movement (kanban boards, category assignment)
When NOT to use it
- For simple drag-to-upload or file drop zones (use native drag events or a file upload library)
- When you need complex nested drag targets (consider using @dnd-kit directly with custom sensors)
Basic usage
Single list reordering
tsx
import { useDragAndDropList } from '@/shared/hooks'
import { DndContext, closestCenter } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
type Task = { id: string; title: string }
export function TaskList({ tasks }: { tasks: Task[] }) {
const {
items,
itemIds,
activeItem,
handleDragStart,
handleDragEnd,
handleDragCancel,
} = useDragAndDropList({
items: tasks,
getItemId: (task) => task.id,
onReorder: (reorderedItems) => {
// Persist new order
console.log('New order:', reorderedItems)
},
})
return (
<DndContext
collisionDetection={closestCenter}
onDragStart={({ active }) => handleDragStart(active.id)}
onDragEnd={({ active, over }) => handleDragEnd(active.id, over?.id ?? null)}
onDragCancel={handleDragCancel}
>
<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
{items.map((task) => (
<SortableItem key={task.id} id={task.id}>
{task.title}
</SortableItem>
))}
</SortableContext>
</DndContext>
)
}Multi-list (kanban-style)
tsx
import { useDragAndDropMultiList } from '@/shared/hooks'
type Card = { id: string; title: string }
type Column = { id: string; title: string; cards: Card[] }
export function KanbanBoard({ columns }: { columns: Column[] }) {
const {
activeItem,
handleDragStart,
handleDragEnd,
handleDragCancel,
} = useDragAndDropMultiList({
lists: columns,
getListId: (col) => col.id,
getListItems: (col) => col.cards,
getItemId: (card) => card.id,
onMoveWithinList: (listId, items, activeId, overId) => {
// Handle reorder within same column
},
onMoveBetweenLists: (fromListId, toListId, itemId, overItemId) => {
// Handle move between columns
},
})
// Render columns with DndContext...
}Companion components
Katalyst provides presentational components in src/shared/ui/components/DragAndDrop.tsx:
DraggableItem— Wrapper for draggable items with drag state stylingDragHandle— Accessible drag handle with grab cursorDroppableZone— Drop target with visual feedbackSortableList— Complete sortable list with built-in DnD context
These components integrate with the hooks and provide consistent visual feedback.
API reference (concise)
useDragAndDropList
ts
interface UseDragAndDropListOptions<T> {
items: T[]
getItemId: (item: T) => UniqueIdentifier
onReorder?: (items: T[], activeId: UniqueIdentifier, overId: UniqueIdentifier) => void
}
interface UseDragAndDropListResult<T> {
items: T[]
itemIds: UniqueIdentifier[]
activeItem: T | null
handleDragStart: (id: UniqueIdentifier) => void
handleDragEnd: (activeId: UniqueIdentifier, overId: UniqueIdentifier | null) => void
handleDragCancel: () => void
reorderItems: (activeId: UniqueIdentifier, overId: UniqueIdentifier) => T[]
}useDragAndDropMultiList
ts
interface UseDragAndDropMultiListOptions<T, L> {
lists: L[]
getListId: (list: L) => UniqueIdentifier
getListItems: (list: L) => T[]
getItemId: (item: T) => UniqueIdentifier
onMoveWithinList?: (listId, items, activeId, overId) => void
onMoveBetweenLists?: (fromListId, toListId, itemId, overItemId) => void
}Related primitives
- useFilter — Often combined with drag and drop for filtered, sortable lists