Skip to content
Domphy

API Reference

dragDrop(state, config?)

import { dragDrop } from "@domphy/dnd"

A Domphy patch applied via $. Wires the FormKit drag-and-drop engine to a reactive state array. Animations are enabled by default — pass animated: false to disable.

const list = createVirtualizer(...)   // or just toState([...])
const App = {
  ul: (l) => items.get(l).map((item) => ({ li: item.label, _key: item.id })),
  $: [dragDrop(items, config?)],
}

dragDrop registers the container via a per-node behavior() instance (@domphy/core): attach runs dragAndDrop() once per real DOM node (animations plugin included by default), later factory re-runs on a reused node route the new state/config into that same instance via update(), and destroy tears down listeners, disconnects FormKit's setup MutationObserver, and drops the parent from FormKit's parents registry. Do not wire FormKit from _onMount/_onRemove — those hooks fire only for generation 1 on a reused node.

state argument

Any object with .get() and .set(updater) methods (Domphy State<T[]> or custom). On reorder, FormKit calls state.set(newArray) so the reactive list re-renders.

config (DragDropConfig)

DragDropConfig<T> extends ParentConfig<T> with one extra field:

interface DragDropConfig<T> extends Partial<ParentConfig<T>> {
  animated?: boolean   // default: true — enable sort animations
}

config (ParentConfig)

interface ParentConfig<T> {
  // Transfer between lists
  group?: string                             // lists with same group can exchange items
  name?: string                              // label for accepts() specificity

  // Sorting
  sortable?: boolean                         // default: true

  // Drag start
  disabled?: boolean                         // disable all dragging in this list
  nativeDrag?: boolean                       // use native HTML5 drag API (default: true)
  multiDrag?: boolean                        // allow Shift/Ctrl multi-select before drag

  // Handles
  draggable?: (el: HTMLElement) => boolean   // filter which children are draggable
  draggableValue?: (value: T) => boolean     // same filter but receives the item value
  dragHandle?: string                        // CSS selector for drag handle inside each item
  externalDragHandle?: {                     // handle element outside the list item
    el: HTMLElement
    callback: () => HTMLElement
  }

  // Drop zone
  accepts?: (
    targetParent: ParentRecord<T>,           // the list being dragged over
    initialParent: ParentRecord<T>,          // the list where the drag started
    currentParent: ParentRecord<T>,          // the list most recently hovered
    state: BaseDragState<T>                  // raw drag state
  ) => boolean
  dropZone?: boolean                         // treat the parent element itself as a drop zone

  // CSS classes applied during drag phases
  draggingClass?: string           // added to the element being dragged
  dropZoneClass?: string           // added to a valid drop zone parent
  dropZoneParentClass?: string     // added to the parent of the active drop zone
  selectedClass?: string           // added to items in a multi-drag selection
  synthDraggingClass?: string      // synthesized drag indicator (non-native drag)
  synthDropZoneClass?: string
  synthDropZoneParentClass?: string
  longPressClass?: string          // added while long press is pending

  // Long press (touch devices)
  longPress?: boolean              // enable long-press-to-drag (default: false)
  longPressDuration?: number       // ms before drag starts (default: 200)

  // Custom drag image
  dragImage?: (data: NodeDragEventData<T>, draggedNodes: NodeRecord<T>[]) => void

  // Thresholds
  threshold?: { horizontal: number; vertical: number }   // fraction 0–1 (default 0.5)

  // Plugins
  plugins?: DNDPlugin[]

  // Callbacks
  onDragstart?: (data: DragstartEventData<T>) => void
  onDragend?: (data: DragendEventData<T>) => void
  onSort?: (data: SortEventData<T>) => void
  onTransfer?: (data: TransferEventData<T>) => void
}

Plugins

Import plugins from @domphy/dnd (re-exported from @formkit/drag-and-drop):

import { animations } from "@domphy/dnd"

animations(config?)

Smooth CSS transitions for dragging and drop. Included by default in dragDrop() — you only need this if you use dragAndDrop() directly or set animated: false:

// Already animated — no extra step needed:
{ ul: ..., $: [dragDrop(items)] }

// Opt out of animations:
{ ul: ..., $: [dragDrop(items, { animated: false })] }

// Custom animation config — disable default, pass explicit plugin:
{ ul: ..., $: [dragDrop(items, { animated: false, plugins: [animations({ duration: 200 })] })] }
interface AnimationsConfig {
  duration?: number          // ms; default 150
  easing?: string            // CSS easing; default "ease-in-out"
  remapFinished?: () => void // callback invoked after nodes finish remapping
  yScale?: number            // Y-axis scale factor during animation
  xScale?: number            // X-axis scale factor during animation
}

Drag handles via dragHandle

dragHandle is a CSS selector config key — not a plugin. Scope dragging to a handle element inside each item:

{ ul: (l) => items.get(l).map((item) => ({
  _key: item.id,
  li: [
    { span: "⠿", class: "handle" },
    { span: item.label },
  ],
})),
$: [dragDrop(items, { dragHandle: ".handle" })] }

Items without a matching .handle element are not draggable by direct drag.


Multi-list helpers

multiList(options)

Convenience adapter for a single list participating in a named drag group. All lists with the same group string accept transfers from each other.

import { multiList } from "@domphy/dnd"

const todo = toState<Task[]>([...])
const done = toState<Task[]>([...])

const TodoList = {
  ul: (l) => todo.get(l).map((t) => ({ li: t.text, _key: t.id })),
  $: [multiList({ group: "kanban", values: todo })],
}

const DoneList = {
  ul: (l) => done.get(l).map((t) => ({ li: t.text, _key: t.id })),
  $: [multiList({ group: "kanban", values: done })],
}

multiListGroup(group, states, config?)

Shorthand when you have an array of lists — returns one patch per list:

import { multiListGroup } from "@domphy/dnd"

const [dropTodo, dropInProgress, dropDone] = multiListGroup("kanban", [todo, inProgress, done])

const Board = {
  div: [
    { ul: (l) => todo.get(l).map(...), $: [dropTodo] },
    { ul: (l) => inProgress.get(l).map(...), $: [dropInProgress] },
    { ul: (l) => done.get(l).map(...), $: [dropDone] },
  ],
}

Transfer between lists (manual)

Two lists with the same group exchange items when an item is dragged from one to the other. The multiList/multiListGroup helpers above do this automatically. Manual equivalent:

const todo = toState<Task[]>([...])
const done = toState<Task[]>([...])

const TodoList = {
  ul: (l) => todo.get(l).map((t) => ({ li: t.text, _key: t.id })),
  $: [dragDrop(todo, { group: "kanban" })],
}

const DoneList = {
  ul: (l) => done.get(l).map((t) => ({ li: t.text, _key: t.id })),
  $: [dragDrop(done, { group: "kanban" })],
}

Use accepts to limit what can transfer in:

dragDrop(done, {
  group: "kanban",
  // Only accept items coming from the "todo" column.
  accepts: (_target, initialParent) => initialParent.el.id === "col-todo",
})

Sortable with server sync

Wire onSort to persist new order after each drag:

dragDrop(items, {
  onSort: async ({ values }) => {
    await api.patch("/tasks/order", { ids: values.map((t) => t.id) })
  },
})

Drag state types

These are the exact types from @formkit/drag-and-drop (re-exported from @domphy/dnd).

// Fired when a drag starts (native or synthetic pointer drag).
interface DragstartEventData<T> {
  parent: ParentRecord<T>         // the list containing the dragged item
  values: T[]                     // current ordered values of that list
  draggedNode: NodeRecord<T>      // the primary dragged DOM node
  draggedNodes: NodeRecord<T>[]   // all dragged nodes (>1 in multi-drag)
  position: number                // starting index of the dragged item
  state: BaseDragState<T>
}

// Fired when a drag ends (drop or cancel).
interface DragendEventData<T> {
  parent: ParentRecord<T>         // the list where the item landed
  values: T[]                     // final ordered values of that list
  draggedNode: NodeRecord<T>
  draggedNodes: NodeRecord<T>[]
  state: BaseDragState<T>
}

// Fired after a reorder within a single list.
interface SortEventData<T> {
  parent: ParentRecord<T>
  previousValues: T[]             // order before the sort
  values: T[]                     // order after the sort
  previousNodes: NodeRecord<T>[]
  nodes: NodeRecord<T>[]
  draggedNodes: NodeRecord<T>[]
  targetNodes: NodeRecord<T>[]
  previousPosition: number        // index before move (0-based)
  position: number                // index after move (0-based)
  state: BaseDragState<T>
}

// Fired after an item crosses from one list to another.
interface TransferEventData<T> {
  sourceParent: ParentRecord<T>   // list the item came from
  targetParent: ParentRecord<T>   // list the item went to
  initialParent: ParentRecord<T>  // list where the drag first started
  draggedNodes: NodeRecord<T>[]
  targetNodes: NodeRecord<T>[]
  targetIndex: number             // insertion index in the target list
  state: BaseDragState<T>
}

ParentRecord<T> exposes { el: HTMLElement; data: ParentData<T> } — access el for the DOM node and data.getValues(el) for the current values. NodeRecord<T> exposes { el: Node; data: NodeData<T> } — access data.value for the item value.


Low-level FormKit API

All @formkit/drag-and-drop exports are re-exported from @domphy/dnd:

import {
  dragAndDrop,      // low-level setup for a container element
  animations,       // plugin: smooth CSS transitions
  insert,           // plugin: insert items with a drop indicator
  dropOrSwap,       // plugin: swap positions instead of shift-sort
  updateConfig,     // update a mounted parent's config without remounting
  parentValues,     // get current values from a parent element
  setParentValues,  // programmatically update a parent's values
  sort,             // low-level sort utility (moves nodes in state)
  transfer,         // low-level transfer utility (moves nodes between parents)
  nodes,            // WeakMap of all registered draggable node elements
  parents,          // WeakMap of all registered parent container elements
  tearDown,         // remove FormKit listeners from a parent element
} from "@domphy/dnd"

See FormKit drag-and-drop docs for the full API surface. The dragDrop adapter is a thin wrapper — anything dragAndDrop() supports is available via config.


Accessibility

The engine is pointer-based — keyboard drag-and-drop is not implemented upstream (handleNodeKeydown is an empty stub; the only built-in key handling is Escape clearing a multi-drag selection), and no aria-grabbed/aria-dropeffect/tabindex attributes are applied automatically. Build keyboard-operable reorder yourself: make items focusable, reorder the bound state from a key handler, and announce the result via an ARIA live region — see Accessibility.