• CMSnpm version

    • Overview
    • ContentBuilder
    • InviteUserForm
    • LoginForm
    • RenewPasswordForm
    • WysiwygInput
  • UInpm version

    • Overview
    • Accordion
    • AlertBubble
    • AnnouncementBar
    • Avatar
    • AvatarFileInput
    • Badge
    • Button
    • ButtonGroup
    • ButtonList
    • Calendar
    • Checkbox
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • Chip
    • ColorRadio
    • ColorRadioGroup
    • Combobox
    • DatePicker
    • DatePickerInput
    • DateRangePicker
    • DateRangePickerInput
    • DatetimePicker
    • DatetimePickerInput
    • Dialog
    • Dropdown
    • Dropzone
    • ErrorMessage
    • FileInput
    • FlashMessages
    • FormComponent
    • Icon
    • IconButton
    • ImageGallery
    • InfoBox
    • Input
    • Label
    • Layout
    • Lightbox
    • ListItem
    • Loader
    • Lozenge
    • Menu
    • Message
    • Modal
    • ✅ ModalDialog
    • ✅ ModalHeader
    • MultiCombobox
    • MultiSelect
    • Pagination
    • Paper
    • Popover
    • Radio
    • RadioGroup
    • RasterImage
    • Select
    • Switch
    • ✅ Tabs
    • TextInput
    • TextLink
    • Textarea
    • TimePicker
    • TimePickerInput
    • Toggle
    • Tooltip
    • Typography
  • Formnpm version

    • Overview
    • AvatarFileInput
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • ColorRadioGroup
    • Combobox
    • DatePickerInput
    • DateRangePickerInput
    • DatetimePickerInput
    • Dropzone
    • FileInput
    • Form
    • FormRenderer
    • GpsInput
    • MoneyInput
    • MultiCombobox
    • MultiSelect
    • NumberInput
    • PasswordInput
    • RadioGroup
    • Select
    • TextInput
    • Textarea
    • TimePickerInput
    • Toggle
  • DataGridnpm version

    • Overview
    • DataGrid
    • DataGridCustomExample
    • ExportButton
    • FilterList
    • Filters
    • FiltersButton
    • FulltextInput
    • HiddenColumns
    • HiddenColumnsButton
    • Pagination
    • RowCounts
    • RowsPerPageSelect
    • SelectedRowsToolbar
    • TableV2
    • ToolbarControl
    • ToolbarCustoms
    • ToolbarTabs
  • Wysiwygnpm version

    • Overview
  • Resizernpm version

    • Overview
  • Routernpm version

    • Overview
  • Corenpm version

    • Overview
  • Core-Reactnpm version

    • Overview
  • DnDnpm version

    • Overview
  • Sentrynpm version

    • Overview
  • Stylesnpm version

    • Overview
  • Localizenpm version

    • Overview
  • Analyticsnpm version

    • Overview
  • Datepickernpm version

    • Overview
  • Icons-generatornpm version

    • Overview
  • Smart-addressnpm version

    • Overview
  • E2Enpm version

    • Overview
  • E2E-Playwrightnpm version

    • Overview

@uxf/dnd

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.

When to use

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, re-exports and a thin DndContext wrapper, no UI 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.

Installation

yarn add @uxf/dnd @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Peer dependencies: @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2, react ^19.3.0, react-dom ^19.3.0.

No CSS and no provider setup. The whole package is client-side; index.ts carries "use client".

Quick start

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>
    );
}

API

Everything is exported from the package root, @uxf/dnd. The hooks are also importable one by one from @uxf/dnd/hooks/<name>.

Hooks

HookSignatureDescription
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. items starts over from initialItems only when its ids or their order change: a new array, even of rebuilt objects, keeps the user's order and shows the latest version of each item. setItems therefore controls order and membership; an item's fields always come from initialItems.
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.

DndContext

dnd-kit's DndContext with one change: without an id prop it takes one from useId. dnd-kit itself numbers its aria-describedby target from a module-level counter, which on a server keeps growing with every request while the browser starts from zero, so every page load after the first failed hydration. Pass id only if you need a specific one.

Re-exported from @dnd-kit/core

DragOverlay, KeyboardSensor, MouseSensor, PointerSensor, TouchSensor, closestCenter, closestCorners, defaultDropAnimationSideEffects, pointerWithin, rectIntersection, useDndContext, useDndMonitor, useDraggable, useDroppable, useSensor, useSensors.

Types: Active, CollisionDetection, DndContextProps, DragCancelEvent, DragEndEvent, DragMoveEvent, DragOverEvent, DragStartEvent, DraggableAttributes, DropAnimation, Modifier, Modifiers, Over, UniqueIdentifier.

Re-exported from @dnd-kit/sortable

SortableContext, arrayMove, arraySwap, horizontalListSortingStrategy, rectSortingStrategy, rectSwappingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy.

Types: AnimateLayoutChanges, SortingStrategy.

Re-exported from @dnd-kit/utilities

CSS, and the Transform type.

Gotchas

  • Import from @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.
  • Items must carry a string 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.
  • When the order lives elsewhere, render your own list and take only 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.
  • Client components only. Every hook uses useState, so the calling module needs "use client" under the Next.js app router.

Links

  • dnd-kit documentation
  • @uxf/data-grid/hidden-columns — useSortableMulti in production
  • @uxf/cms/pages/content-builder — useSortableSingle driving a react-hook-form field array