# Drag and Drop

Sort a list, nest a tree, or move items between lists with a pointer or keyboard.

`createReorder` sorts lists and grids, `createTreeReorder` moves rows and subtrees, and `createTransfer` moves items between lists. They keep a draft during a move and commit it through your [change](/docs/core/kernel/handle-changes#changes) on drop. Use `bindDrag` to track a pointer for a custom drag.

Import these helpers from `@ui-sdk/core`. [Compose Behaviors](/docs/core/compose-behaviors) shows how to connect a behavior to a primitive. For freely positioned items, see [Dragging and Resizing](/docs/core/behaviors/direct-manipulation).

## Pointer Drags

### Follow a Pointer

`bindDrag` follows the pointer that pressed an element. Call it from a pointer handler, and keep its cleanup so destroying the primitive also stops the drag:

```ts
let stop: (() => void) | undefined
context.onDestroy(() => stop?.())

return {
  pointerdown(event: PointerEvent, el: HTMLElement) {
    if (event.button !== 0) {
      return
    }
    stop?.()
    stop = bindDrag(el, event, {
      threshold: dragThreshold,
      cancelOnEscape: true,
      onMove: ({ x }) => resizeTo(x),
      onEnd: (_event, canceled) => (canceled ? restore() : commit()),
    })
  },
}
```

Here `resizeTo`, `restore`, and `commit` are your splitter's own methods. `bindDrag` only tracks the pointer; you decide what each position means and add the keyboard controls.

Drag the divider, or focus it and use Left and Right. Press Escape during a drag to restore the width.

`onMove` runs at most once a frame. `onEnd` receives a cancellation flag if the browser takes over the pointer or Escape cancels it. The returned cleanup stops tracking without calling `onEnd`.

`dragThreshold` waits for a few pixels of movement before `onMove` starts. A press can still end below that threshold, so don't assume every `onEnd` follows a move. Give a draggable surface `touch-action: none` when dragging should take precedence over scrolling.

### Collapse Past a Threshold

A sidebar resizes between its minimum and maximum widths, and can collapse, but never rests in between. Below the minimum, let `scrubTo` decide: give it two snap points, collapsed and the minimum, each reaching halfway, and ease toward them:

```ts
import { scrubTo } from "@ui-sdk/core/number"

const collapse = { min: 0, max: minWidth, step: 1, snap: { points: [0, minWidth], threshold: minWidth / 2 } }

const widthAt = (pointer: number) =>
  pointer >= minWidth ? { value: Math.min(maxWidth, pointer), point: undefined } : scrubTo(pointer, collapse, { ease: true })

onMove: ({ x }) => {
  const { value, point } = widthAt(x - root.getBoundingClientRect().left)
  store.set({ draft: value, landing: point ?? null })
},
onEnd: () => commit(store.get().landing ?? store.get().draft),
```

While the pointer moves below the minimum, the sidebar follows it, picking up speed toward whichever point is closer, so it closes in on collapsing without a sudden jump. `point` says where it will land, so the demo can say so before release; dragging the other way opens it the same way.

Drag the divider below the sidebar's minimum: it eases toward collapsing or back to the minimum, and says which it will do when you let go. Focus the divider for Left, Right and Enter.

### Dragging Without Limits

For a scrubber, pointer lock lets the user keep dragging past the edge of the screen. Read the distance moved instead of the pointer's position:

```ts
bindDrag(el, event, {
  threshold: dragThreshold,
  lock: true,
  onStart: () => !context.options().locked,
  onMove: ({ movementX, event }) => {
    change("value", context.store.get().value + movementX / 2, {
      reason: "drag",
      event,
    })
  },
  onEnd: () => {},
})
```

`onStart` runs when the movement reaches the threshold. Returning `false` stops tracking without calling `onEnd`. If the browser can't lock the pointer, tracking continues with pointer capture.

Drag the Opacity label left or right. When pointer lock is available, the cursor hides so you can keep going. Check Locked to prevent a drag.

The sample passed to `onMove` is reused. Read `x`, `y`, `movementX`, `movementY`, `locked`, and `event` during the callback instead of saving the object for later.

## Reordering

### Sort a List

Keep the committed order in a `string[]` and start a separate `ReorderDraft | null` field at `null`. In `api`, connect them to `createReorder`:

```ts
const change = createChange(context)
const reorder = createReorder(context, {
  slot: "item",
  key: "value",
  draft: "moving",
  set: (value, details) => change("value", value, details),
})
```

Return `reorder` from `api`. Render items from `reorder.order(state)`, which reads the draft during a move and the committed value otherwise. Keep each item's key stable when the order changes.

The item slot runs the reorder handler before its existing roving focus handler. That lets arrows move focus until someone picks up an item:

```ts
on: {
  keydown: (event, { el, api, key }) =>
    api.reorder.keydown(event, key) || api.roving.keydown(event, el!),
  pointerdown: (event, { api, key }) => api.reorder.pointerdown(event, key),
},
bind: (el, part) => {
  const cleanups = [part.api.roving.bind(el, part), part.api.reorder.bind(el, part)]
  return () => cleanups.forEach((cleanup) => cleanup())
},
```

Drag a season to rank it. With the keyboard, focus one, press Space or Enter, move it with the arrows, then press Space or Enter to drop it. Escape cancels.

Home and End move a picked-up item to either end. Tab cancels the move and continues through the page. Changes report `drag` or `move-key`, and screen readers hear the pickup, moves, and drop.

This demo uses a `listbox` of `option`s, `aria-roledescription` for the sortable list and items, and `aria-selected` for the picked-up item. Give each item an `aria-describedby` pointing to keyboard instructions. For items with nested controls, use a list with a separate drag handle button and put the handlers on that handle.

While dragging, core writes `--drag-x` and `--drag-y` onto the bound item. Use them to keep it under the pointer, with a `data-grabbed` attribute derived from `state.moving?.key === key`:

```css
[data-slot="sortable-list-item"] {
  touch-action: none;
  user-select: none;
}
[data-slot="sortable-list-item"][data-grabbed] {
  transform: translate(var(--drag-x, 0), var(--drag-y, 0));
  position: relative;
  z-index: 1;
}
```

### Grids

The default orientation is vertical. Add `orientation: () => "grid"` for items that wrap into rows:

```ts
orientation: () => "grid",
```

Drag a photo to a new position, or pick it up with Space. Left and Right move one place; Up and Down move between rows. Space drops it.

For a single horizontal row, use `"horizontal"`, give its roving focus the same orientation, and set `aria-orientation="horizontal"` on the listbox.

### Several at Once

A `group` callback lets a selection move together. Add it to the reorder options:

```ts
group: (key) => {
  const { selected } = context.store.get()
  return selected.includes(key) ? selected : [key]
},
```

The group is captured when the drag starts, keeps the list's order, and leaves disabled items behind. `reorder.moving(state, key)` identifies every item in the group; `reorder.dragging(state, key)` also requires a pointer drag.

Check a few tasks, then drag one of the checked tasks. You can also focus a checked task and use Space and the arrows to move the group.

A pointer event already handled with `preventDefault()` doesn't start a reorder. That lets a checkbox handle its press without also picking up the item.

### A Copy While Dragging

For a scrolling list, a translated item may be clipped. Add `preview: true` to drag a copy above the page:

```ts
preview: true,
createAutoScroll: { edge: 40, speed: 12 },
```

Drag a track and hold it near the playlist's top or bottom edge to scroll. Its placeholder shows where it will drop.

The original stays in the proposed order. With a group, the copy also gets `data-drag-count` and `--drag-count` for a count badge. Auto-scrolling is enabled with the preview; use `createAutoScroll: false` to turn it off.

### Moving from Code

Use `move` for an immediate change, such as a menu's Move to Top action:

```ts
reorder.move("Serve", 0)
```

For controls that work one step at a time, use `grab(key)`, `moveTo(index)`, and `drop()` or `cancel()`. `moveTo` counts positions among the items left after removing the moving group.

Use Top to move a recipe step immediately. The toolbar picks up the focused step, moves it, and either drops it or cancels.

Immediate moves report `imperative`; pickup and drop controls follow the same draft as the keyboard.

### What's Read Out

Use `label` when an item's visible text contains more than its name. A custom `message` can replace just one announcement and keep the defaults for the rest:

```ts
disabled: (item) => item.key === context.options().pinned,
label: (key) => titles[key],
message: (message) =>
  message.type === "move" ? `${message.label}, track ${message.position}.` : reorderMessage(message),
```

Pick up an unpinned track with Space and move it with the arrows. A screen reader announces track numbers. The pinned track can't be picked up, but other tracks can move past it.

`disabled` prevents pickup; it doesn't reserve an item's position. Announcements are assertive, and several changes in one turn may be announced together as the latest message.

## Reordering a Tree

### Sort and Nest

A tree uses a flat array of `{ key, depth }` rows in reading order. Moving a parent moves its whole subtree. Start `moving` at `null` with the type `TreeReorderDraft | null`:

```ts
const tree = createTreeReorder(context, {
  key: "rows",
  draft: "moving",
  set: (value, details) => change("rows", value, details),
  indent: () => 20,
  preview: true,
})
```

Render `tree.rows(state)`, indent by `depth`, and bind each rendered row with `tree.bind(el, key)`. Keep the roving focus binding too. As with a sortable list, call `tree.keydown(event, key)` before the focus handler and `tree.pointerdown(event, key)` on a press.

Drag an outline row up or down, and sideways to change its depth. With the keyboard, pick it up with Space, use Up and Down to place it, and Left and Right to change its level.

`indent` is the horizontal distance for one level, 24 pixels by default. Closed subtrees can remain in the data while their rows are unmounted; pointer targeting skips those hidden rows.

A move won't change a neighboring row's parent. Depth is limited by the rows around the insertion point. The `set` details include `key`, `parent` (`null` at the root), and the zero-based `index` among that parent's children, which is useful when your application stores a nested tree.

### Limits and Moving from Code

Add a maximum depth and a pickup rule:

```ts
maxDepth: () => 1,
disabled: (key) => key === "Inbox",
```

Depth starts at zero, so this allows two levels. The limit includes the moving row's descendants.

Use a folder's arrow buttons to indent or outdent it with its contents. The tree allows two levels, and Inbox can't be picked up.

`tree.move(key, before, depth)` moves a subtree before another row, or to the end when `before` is `null`. It uses the nearest depth that fits. `subtreeEnd(rows, index)` returns the first index after a subtree, which helps calculate where to insert it.

For a staged move, use `grab`, `place`, then `drop` or `cancel`. `moving(state, key)` includes descendants; `dragging` also checks that the move came from a pointer. Customize `label` and `message` as for a list, with `treeReorderMessage` as the default wording.

## Moving Between Lists

### Two Lists

Store the lists together as a `Record<string, string[]>`, with item keys unique across all lists. Add a `TransferDraft | null` field for the move:

```ts
const transfer = createTransfer(context, {
  key: "lists",
  draft: "moving",
  set: (value, details) => change("lists", value, details),
})
```

Render from `transfer.lists(state)`. Bind list elements with `bindList(el, key)` and item elements with `bindItem(el, key)`, returning their cleanups from the slots. Keep the existing focus binding, and run `transfer.keydown(event, key)` before its key handler. A press calls `transfer.pointerdown(event, key)`.

Drag an item between packing lists. With the keyboard, pick it up with Space, use Left and Right to change lists and Up and Down to place it, then Space to drop it.

Transfer uses a dragged copy and auto-scrolls near edges. `moving(state, key)` identifies the picked-up item, while `dragging(state, key)` identifies a pointer drag. The `set` details include the item's `key`, its `from` and `to` lists, and its new `index`.

### A Board with Rules

Use `accepts` to reject a destination, and `order` to choose how the keyboard travels between lists:

```ts
order: () => ["todo", "doing", "done"],
accepts: (key, list) =>
  list !== "doing" || context.store.get().lists.doing.filter((card) => card !== key).length < 2,
disabled: (key) => key === "Billing",
listLabel: (list) => columns[list],
```

Move cards between columns. Doing holds at most two cards, Billing can't be picked up, and the check on a card moves it straight to Done.

`transfer.move("Search", "done", 0)` makes an immediate move. For a staged move, use `grab`, `moveTo(list, index)`, then `drop` or `cancel`.

`label` names items, `listLabel` names destinations, and `message` customizes announcements with `transferMessage` as the default. Set `createAutoScroll: false` for a board that doesn't need edge scrolling.

## Dragging a Copy

For your own drag, `bindDragPreview` creates a copy of an element that follows the pointer. The original stays in place:

```ts
const preview = bindDragPreview(el, { x: event.clientX, y: event.clientY })
```

In the drag's `onMove`, call `preview.move(x, y)`. In `onEnd` and any early cleanup, call `preview.remove()`. You can style the copy through `preview.el`, for example by toggling a `data-over` attribute over a valid target.

Drag a tag onto the note and release it when the copy is highlighted. You can also focus a tag and press Enter to add it.

The copy preserves the element's appearance when created; it doesn't render future framework updates. It has `data-drag-preview`, stripped IDs, and is hidden from assistive technology and pointer targeting. It uses the top layer when available, with a fixed-position fallback.

## Scrolling While Dragging

A custom drag can use `createAutoScroll` to scroll near the pointer. Create it for the drag, call `update(x, y)` on each move, and call `stop()` on every exit:

```ts
const scroll = createAutoScroll({
  edge: 48,
  speed: 16,
  ignore: () => preview.el,
  onScroll: findChapter,
})
```

Drag the bookmark near the chapter list's top or bottom edge. Keep holding it there to scroll, then release it over a chapter.

`edge` is the distance in pixels where scrolling begins; `speed` is the maximum pixels per frame. The helper finds the nearest scroller under the pointer, with vertical page scrolling as a fallback. `ignore` skips an element such as the dragged copy. `onScroll` lets you find the target again after the content moves under a stationary pointer.

## Keeping Focus and the Draft

The sorting behaviors already preserve focus and keep a committed draft visible while a controlled value catches up. If you're building your own drag, use [`keepFocus`](/docs/core/behaviors/focus#focus-that-follows-a-move) after moving a focused element and [`settleDraft`](/docs/core/kernel/handle-changes#changes-from-elsewhere) when handing a draft to its owner.

## API Reference

The option tables link back to the examples that introduce them.

- `onMove` (`(sample: PointerSample) => void`): At most once per frame, with the latest position and the movement since the last call. Example: [Follow a Pointer](#follow-a-pointer).
- `onEnd` (`(event: PointerEvent, canceled: boolean) => void`): The pointer lifted, or the browser took it over, like for a scroll. A move still pending runs first, so the last position is never lost. Example: [Follow a Pointer](#follow-a-pointer).
- `threshold?` (`number`): How far, in CSS pixels, the pointer moves along either axis before moves are reported, so a press that barely moves stays a click. Defaults to 0. Example: [Follow a Pointer](#follow-a-pointer).
- `onStart?` (`((event: PointerEvent) => boolean | void)`): Runs once, on the first move past `threshold`, before its `onMove`. Return `false` to stop following the pointer, like when a drag can't start. Example: [Dragging Without Limits](#dragging-without-limits).
- `lock?` (`boolean`): Locks the pointer once it has moved past `dragThreshold`, so it can travel without limit, like a scrub, while a plain click never hides the cursor. Falls back to capture where the browser refuses, as Safari does. Example: [Dragging Without Limits](#dragging-without-limits).
- `cancelOnEscape?` (`boolean`): Ends the gesture as canceled when Escape is pressed once it has started, and keeps that press from reaching anything else, like a popup around it. `onEnd` gets the last pointer event. Defaults to `false`. Example: [Follow a Pointer](#follow-a-pointer).

- `slot` (`string`): The slot whose parts are the items. Each joins in the slot's `bind` with `reorder.bind(el, part)`. Example: [Sort a List](#sort-a-list).
- `key` (`keyof State & string`): The state key that holds the order, a `string[]` of item keys. Example: [Sort a List](#sort-a-list).
- `draft` (`keyof State & string`): The state key that holds the `ReorderDraft` while an item moves. Start it at `null`. Example: [Sort a List](#sort-a-list).
- `set` (`(value: string[], details: { reason: ReorderReason; event?: Event; }) => boolean`): Asks for a new order and returns whether it went through. Pass the primitive's change, so `on<Key>Change`, `cancel()` and controlled values work: `set: (value, details) => change("value", value, details)`. Example: [Sort a List](#sort-a-list).
- `orientation?` (`(() => ReorderOrientation)`): Whether the items run top to bottom, along a line, or wrap in rows like a grid of photos, which picks the arrows and how a drag finds its place. Defaults to `"vertical"`. Example: [Grids](#grids).
- `group?` (`((key: string) => string[])`): The keys that move with the one picked up, like the selection when it holds it. Defaults to that one key. Example: [Several at Once](#several-at-once).
- `preview?` (`boolean`): Drags a copy of the item above the page while the item itself shows where it would drop, so a list that scrolls neither clips it nor grows under it. A drag near a scroller's edge scrolls it. Defaults to `false`. Example: [A Copy While Dragging](#a-copy-while-dragging).
- `createAutoScroll?` (`false | Omit<AutoScrollOptions, "ignore" | "onScroll">`): How a drag with `preview` near a scroller's edge scrolls it, or `false` not to. Example: [A Copy While Dragging](#a-copy-while-dragging).
- `disabled?` (`((item: { key: string; props: Props; }) => boolean)`): Whether an item can't be picked up. It still moves aside for others. Example: [What's Read Out](#whats-read-out).
- `label?` (`((key: string) => string | undefined)`): The name screen readers hear for an item. Return `undefined` for the default, its text. Example: [What's Read Out](#whats-read-out).
- `message?` (`((message: ReorderMessage) => string)`): The text screen readers hear for each step. Defaults to English. Example: [What's Read Out](#whats-read-out).

- `key` (`keyof State & string`): The state key that holds the rows, a `TreeReorderRow[]` in reading order. Example: [Sort and Nest](#sort-and-nest).
- `draft` (`keyof State & string`): The state key that holds the `TreeReorderDraft` while an item moves. Start it at `null`. Example: [Sort and Nest](#sort-and-nest).
- `set` (`(value: TreeReorderRow[], details: { reason: TreeReorderReason; event?: Event; key: string; } & TreePlace) => boolean`): Asks for the new rows and returns whether they went through. Pass the primitive's change: `set: (value, details) => change("rows", value, details)`. `details` says which item moved and where to. Example: [Sort and Nest](#sort-and-nest).
- `indent?` (`(() => number)`): How far a drag goes sideways for each level, in CSS pixels. Defaults to 24. Example: [Sort and Nest](#sort-and-nest).
- `maxDepth?` (`(() => number)`): The deepest an item and the rows under it can go, from 0. Defaults to no limit. Example: [Limits and Moving from Code](#limits-and-moving-from-code).
- `disabled?` (`((key: string) => boolean)`): Whether an item can't be picked up. It still moves aside for others. Example: [Limits and Moving from Code](#limits-and-moving-from-code).
- `label?` (`((key: string) => string | undefined)`): The name screen readers hear for an item. Defaults to its text. Example: [Limits and Moving from Code](#limits-and-moving-from-code).
- `message?` (`((message: TreeReorderMessage) => string)`): The text screen readers hear for each step. Defaults to English. Example: [Limits and Moving from Code](#limits-and-moving-from-code).
- `preview?` (`boolean`): Drags a copy of the row above the page while the rows show where it would drop. Defaults to `false`. Example: [Sort and Nest](#sort-and-nest).
- `createAutoScroll?` (`false | Omit<AutoScrollOptions, "ignore" | "onScroll">`): How a drag with `preview` near a scroller's edge scrolls it, or `false` not to. Example: [Sort and Nest](#sort-and-nest).

- `key` (`keyof State & string`): The state key that holds every list, a `TransferLists`. Example: [Two Lists](#two-lists).
- `draft` (`keyof State & string`): The state key that holds the `TransferDraft` while an item moves. Start it at `null`. Example: [Two Lists](#two-lists).
- `set` (`(value: TransferLists, details: { reason: TransferReason; event?: Event; key: string; from: string; to: string; index: number; }) => boolean`): Asks for the new lists and returns whether they went through. Pass the primitive's change: `set: (value, details) => change("lists", value, details)`. `details` says what moved, from where to where. Example: [Two Lists](#two-lists).
- `order?` (`(() => string[])`): The lists in the order Left and Right go through them. Defaults to the order of the value's keys. Example: [A Board with Rules](#a-board-with-rules).
- `accepts?` (`((key: string, list: string) => boolean)`): Whether a list takes an item, like a column at its limit. Lists that don't are skipped. Example: [A Board with Rules](#a-board-with-rules).
- `disabled?` (`((key: string) => boolean)`): Whether an item can't be picked up. Example: [A Board with Rules](#a-board-with-rules).
- `label?` (`((key: string) => string | undefined)`): The name screen readers hear for an item. Defaults to its text. Example: [A Board with Rules](#a-board-with-rules).
- `listLabel?` (`((list: string) => string | undefined)`): The name screen readers hear for a list. Defaults to its key. Example: [A Board with Rules](#a-board-with-rules).
- `message?` (`((message: TransferMessage) => string)`): The text screen readers hear for each step. Defaults to English. Example: [A Board with Rules](#a-board-with-rules).
- `createAutoScroll?` (`false | Omit<AutoScrollOptions, "ignore" | "onScroll">`): How a drag near a scroller's edge scrolls it, or `false` not to. Example: [A Board with Rules](#a-board-with-rules).

- `edge?` (`number`): How far in from an edge scrolling starts, in pixels. Example: [Scrolling While Dragging](#scrolling-while-dragging).
- `speed?` (`number`): The fastest it scrolls, in pixels per frame, with the pointer at the edge. Example: [Scrolling While Dragging](#scrolling-while-dragging).
- `onScroll?` (`(() => void)`): Runs after each frame that scrolled, so a drag can find its target again under a pointer that didn't move. Example: [Scrolling While Dragging](#scrolling-while-dragging).
- `ignore?` (`(() => Element | null | undefined)`): The element being dragged, which sits under the pointer, so the scrollers under it are found instead of its own. Example: [Scrolling While Dragging](#scrolling-while-dragging).
