Drag and drop for UXF apps: a curated facade over @dnd-kit plus the sortable-list hooks shared by @uxf/data-grid and @uxf/cms.
Reach for this package whenever an app needs drag and drop. It exists so that a consumer declares one dependency instead of three @dnd-kit/* packages, and so that @dnd-kit is installed only where drag and drop is actually rendered.
It is not a UI package: it ships hooks and re-exports, no components and no CSS. The draggable markup stays in your app (see @uxf/data-grid/hidden-columns for a worked example).
Before this package existed the same hooks lived in @uxf/core-react/dnd, which forced @dnd-kit onto every consumer of @uxf/core-react. Importing them from @uxf/core-react no longer works — see docs/migration/dnd-package-extraction.md.
yarn add @uxf/dnd @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesPeer dependencies: @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2, react >=18.2.0, react-dom >=18.2.0.
No CSS and no provider setup. The whole package is client-side; index.ts carries "use client".
A single sortable list, wired with useSortableSingle:
"use client";
import {
CSS,
DndContext,
DragOverlay,
SortableContext,
useSortable,
useSortableSingle,
verticalListSortingStrategy,
} from "@uxf/dnd";
import React, { CSSProperties } from "react";
interface Item {
id: string;
label: string;
}
function Row(props: Item) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.id });
const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition };
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{props.label}
</div>
);
}
export function SortableList(props: { items: Item[] }) {
const { items, activeItem, onDragStart, onDragEnd, onDragCancel, sensors, dropAnimationConfig } = useSortableSingle(
props.items,
);
return (
<DndContext onDragCancel={onDragCancel} onDragEnd={onDragEnd} onDragStart={onDragStart} sensors={sensors}>
<SortableContext items={items.map((i) => i.id)} strategy={verticalListSortingStrategy}>
{items.map((item) => (
<Row key={item.id} {...item} />
))}
</SortableContext>
<DragOverlay dropAnimation={dropAnimationConfig}>{activeItem ? <Row {...activeItem} /> : null}</DragOverlay>
</DndContext>
);
}
Everything is exported from the package root, @uxf/dnd. The hooks are also importable one by one from @uxf/dnd/hooks/<name>.
| Hook | Signature | Description |
|---|---|---|
useSortableCore | () => { activeElement, setActiveElement, onDragStart, onDragCancel, sensors, dropAnimationConfig } | The shared pieces every list needs: pointer + keyboard sensors, the active-element state, and a drop animation that fades the dragged item to 0.4. |
useSortableSingle | <T extends { id: string }>(initialItems: T[], onReorder?: (fromIndex, toIndex, updatedList: T[]) => void) => … | One list. Owns the item array, applies arrayMove on drop and calls onReorder. Returns items, itemsIds and setItems on top of the useSortableCore result. |
useSortableMulti | <T extends { id: string }, SectionMap extends Record<string, T[]>>(sections, setSections, onMove?) => … | Several lists with items moving between them. State stays with the caller; the hook computes the next SectionMap and calls onMove. |
useSortableMulti also exports the two placeholder ids it drops onto empty sections:
EMPTY_VISIBLE_COLUMNS_PLACEHOLDER_ID and EMPTY_HIDDEN_COLUMNS_PLACEHOLDER_ID. Render one as a droppable
whenever a section is empty, otherwise there is no drop target to aim at. getSectionItemsIds(section) returns
the right id for you.
@dnd-kit/coreDndContext, DragOverlay, KeyboardSensor, MouseSensor, PointerSensor, TouchSensor, closestCenter, closestCorners, defaultDropAnimationSideEffects, pointerWithin, rectIntersection, useDndContext, useDndMonitor, useDraggable, useDroppable, useSensor, useSensors.
Types: Active, CollisionDetection, DragCancelEvent, DragEndEvent, DragMoveEvent, DragOverEvent, DragStartEvent, DraggableAttributes, DropAnimation, Modifier, Modifiers, Over, UniqueIdentifier.
@dnd-kit/sortableSortableContext, arrayMove, arraySwap, horizontalListSortingStrategy, rectSortingStrategy, rectSwappingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy.
Types: AnimateLayoutChanges, SortingStrategy.
@dnd-kit/utilitiesCSS, and the Transform type.
@uxf/dnd, never from @dnd-kit/* directly. dnd-kit keeps its state in a React context, so two copies in one tree fail silently: drags start but never find a drop target. Going through the facade keeps a single version resolved for the whole app.id. useSortableSingle and useSortableMulti both constrain T to { id: string }, and SortableContext is fed those same ids.useSortableMulti's empty-section placeholders assume sections named visible and hidden. Dropping onto a placeholder resolves the destination to one of those two literal keys, so a SectionMap keyed differently will move the item into a section that does not exist. Non-empty sections work with any keys.itemsIds and activeItem. That is what @uxf/cms does: it renders react-hook-form's fields and feeds onReorder's first two arguments straight into the field array's move(). The hook's own items copy is then just the bookkeeping behind those two values.useState, so the calling module needs "use client" under the Next.js app router.@uxf/data-grid/hidden-columns — useSortableMulti in production@uxf/cms/pages/content-builder — useSortableSingle driving a react-hook-form field array