Raft Design System

Components · Actions

Drag

The parts every drag in Raft is built from — one sensor set, the lifted copy that follows the hand, the dashed place where an item will land, the reason a place refuses it, and announcements by name.

ExperimentalAlso called Drag and drop, DnD, DragLift, DropPlaceholder, DragRefusalHint, useDragSensors, useDragLists, Sortable
import { Drag } from "@botiverse/raft-ui/wip";

The Drag parts are what every drag in Raft is built from, on dnd-kit: useDragSensors starts a drag the same way everywhere, DragLift is the copy that follows the hand and settles on a drop or a cancel, DropPlaceholder is the one look of where an item will land, and DragRefusalHint says why a place won't take it. The model they implement, and its reasons, is in Drag and drop. Import them from @botiverse/raft-ui/wip: the API can still change while Raft Chat moves its drags onto it.

drag/compositions/lists

When to use

SituationUse
Reordering items in a list, or moving them between lists (a sidebar's sections)useDragLists with DraggedSlot, DragLift and useDragSensors
Moving task cards on a boardTaskDnd, built on these parts
Reordering tabsSortableTabsList
Reordering composer attachmentsSortableComposerAttachments
Moving a section of a layout (side sections)useDragSensors, DragLift with a compact proxy, DropPlaceholder
Resizing a columnResizable

Anatomy

  • useDragSensors(): the one sensor set for a DndContext. A mouse or a pen after 6px (MOUSE_DRAG_DISTANCE_PX); a finger after a 500ms hold and a 10px move (useHoldThenMoveTouchSensor); the keyboard on Space (DRAG_KEYBOARD_CODES), never Enter. Pass coordinateGetter (or only the getter, useDragSensors(getter)) for how the arrows move. Escape during a drag from any of them only puts the item back: it is taken in the capture phase on the window and stopped, so no app Escape handler (a panel, a dialog, a popover) hears it until the drag is over.
  • DragLift: the lifted copy, inside the DndContext. Its children are the copy: the item itself, or a compact proxy. It takes the control radius; pass the item's own in className. It also sets the grabbing cursor, cancels on window blur and a hidden tab, scrolls the area the pointer is near the edge of, and sets a keyboard drag's copy 6px off its place. Turn dnd-kit's own auto-scroll off (autoScroll={false}). The copy renders in document.body by default, in a portal that keeps the theme scope it was rendered in (light / dark) and the item's writing direction (dir), so nothing around the item holds it in: neither a stacking context it couldn't rise out of (a ScrollArea, an isolate) nor a transform (a drawer, a dialog) that would offset it. Among the body's layers it sits on top (DRAG_LIFT_Z_INDEX, 999: over popovers, stacked dialogs and toasts). container renders it in another element, and null in place, where it rises no higher than the stacking context around it. The copy inherits nothing else from where it was picked up (its font, colour, or a custom property a container sets, such as a row height), so give it its own styles.
  • keepGrabInside(proxyWidth): a modifier for a compact proxy, so the hand stays on it when the item was grabbed far from its start. LIFT_CLASS is the lift look for a copy drawn without DragLift.
  • DropPlaceholder: where the item will land, at the item's size. opaque in a layout's gap or new-column strip, so the column behind doesn't show through.
  • DragRefusalHint: the reason a place refuses the item, in a HUD chip by the pointer, with the not-allowed cursor. Render it only while the place under the hand refuses. The chip sits right of and below the pointer; near the window's right edge it goes left of the pointer and of the copy, near the bottom edge above the pointer. It never sits under the copy and always stays 8px inside the window. Its size is measured when its content changes, not every frame.
  • DraggedSlot: wraps each item's content in a list; pass dragging={isDragging} (it is false by default). The dragged item's own slot is at 40% (ORIGIN_OPACITY) while home and the place, with the item's radius, once it stands elsewhere.
  • useDragLists: items in one or more lists, reordered and moved between them, with refusals and announcements by name. It returns the DndContext's handlers, collision detection and accessibility, and the coordinateGetter for useDragSensors. Render DragRemeasure version={items} inside the context.
  • useDragAcrossLists, listKeyboardCoordinates and DragRemeasure: the keyboard fix useDragLists is built on, for a surface with lists of its own.
  • createDragAnnouncements, dragInstructions, DEFAULT_DRAG_MESSAGES: announcements and instructions for dnd-kit's accessibility, by name, in your language. A source whose name is empty says nothing. useDragAnnouncer and DragLiveRegion for a surface that announces on its own.
  • useDragSiblingTransition() (or DRAG_SIBLING_TRANSITION): the 160ms slide for useSortable({ transition }), none with reduced motion. DROP_GLIDE is the glide DragLift uses, and DRAG_SETTLE its duration and easing.
  • cancelDrag(): cancels the drag in progress, whichever sensor holds it. It sends the window a resize, which every dnd-kit sensor cancels on, never a synthetic Escape, so no dialog, panel or popover that closes on Escape hears it.
  • Data attributes the stylesheet reads: data-drag-origin on the item a drag came from (40%), data-drag-into on a container the item has moved into (fill-subtle).

States

StateLook
LiftedThe copy: layer-popover, shadow-float, a line-seam ring; no scale, no tilt. The cursor is grabbing everywhere
HomeThe item stays in its place at 40%
Over a placeDropPlaceholder: 1.5px dashed line-strong on fill-subtle, the item's radius and size; opaque on layer-canvas in a layout's gap or new-column strip
Over a refused placeNo placeholder; the DragRefusalHint chip by the pointer; the cursor is not-allowed
Dropped or cancelledThe copy glides into the place, or home, in 160ms; then the item shows there

Behavior

  • The copy follows the pointer 1:1 from the grab point; never clamp it or hold it to one axis with a modifier. A compact proxy may take a modifier that keeps it under the hand.
  • dnd-kit measures the overlay's first element, so the keyboard offset is drawn inside it and moves no drop target.
  • With prefers-reduced-motion, neither the copy nor the siblings animate.
  • Escape during a pointer or a keyboard drag cancels it: the copy glides home, and the keydown goes no further (preventDefault, stopImmediatePropagation). After the drag, Escape behaves as everywhere else.
  • The refusal chip stays inside the window and off the copy: flipped left of the pointer and the copy at the right edge, above the pointer at the bottom edge, held 8px inside on every side.

Accessibility

  • The parts wrap dnd-kit. A draggable item carries dnd-kit's attributes: it is focusable, with a role description and the instructions.
  • Keyboard: Space picks up, the arrow keys move one place, Space drops, Escape cancels (and only cancels: no app Escape handler hears it). Enter never starts a drag. With useDragLists, or useDragAcrossLists and DragRemeasure, each arrow moves exactly one place, also right after a move into another list.
  • Pass names, never ids: createDragAnnouncements({ name, place, refusal }, messages). The English defaults are in DEFAULT_DRAG_MESSAGES; pass your translations as messages, each built from the same details. With no name for the item, say nothing rather than its id: SortableTabsList names a tab by its aria-label or text, SortableComposerAttachments an attachment by its label, and TaskDndProvider a card by its describe, silent without it.
  • DropPlaceholder and DragRefusalHint are hidden from assistive technology: announce the reason as well.

Rules

  • Use useDragSensors for every drag, so a surface never picks its own distance, hold or keys.
  • Render the copy in DragLift, never by moving the item itself.
  • Keep the item in its place while it is dragged, and let data-drag-origin fade it.

API

DragRemeasure

ExperimentalMeasures every drop target again once an item's move into another list has rendered. dnd-kit's own observers skip targets whose size didn't change, even when they moved. Render it inside the `DndContext`.
PropTypeDefault
version (required)Anything that changes when an item moves list, such as the lists' items.{}

DragLift

ExperimentalThe copy of a dragged item that follows the hand (design/patterns/drag-and-drop.md#what-follows-the-hand): an opaque popover surface with `shadow-float` and a `line-seam` ring, no scale and no tilt, 1:1 with the pointer from the grab point, never clamped to its container and never held to one axis. On a drop it glides into its place, and on a cancel back home (160ms, `motion-drag-settle-*`; none with reduced motion).
PropTypeDefault
autoScrollScroll whatever scrollable area the pointer comes within 48px of the edge of. Turn dnd-kit's own off (`<DndContext autoScroll={false}>`) so the two don't scroll together.booleantrue
childrenThe lifted copy: the dragged item itself, or a compact proxy for a large one. Nothing while nothing is lifted.ReactNode
containerWhere the copy renders: `document.body` by default, so no ancestor holds it in, neither a stacking context (a ScrollArea, an `isolate`) it couldn't rise out of nor a transform (a drawer, a dialog) that would offset a `position: fixed` copy. Another element renders it there, and `null` in place. It keeps the theme scope it was rendered in (`light` / `dark`), as raft-ui's portals do, and the item's writing direction (`dir`). Nothing else is inherited from where it was picked up (font, colour, custom properties a container sets): give the copy its own styles.Element | DocumentFragment
modifiersModifiers for the copy only, such as one that keeps a compact proxy under the hand.Modifier[]
zIndexnumberDRAG_LIFT_Z_INDEX

DragRefusalHint

ExperimentalShown while the hand is over a place that refuses the item (design/patterns/drag-and-drop.md#refused-places): the reason, in a HUD chip by the pointer (by the lifted copy in a keyboard drag), and the not-allowed cursor. Render it only while the place is refused, inside the `DndContext`, with no placeholder anywhere. Announce the reason as well: the chip is not read out.
PropTypeDefault
children (required)Why the place under the hand won't take the item, such as "Can't move from Todo to Done".string
containerRender the hint in another element; by default it renders in `document.body`.Element | DocumentFragment

Also takes the attributes of its element.

DropPlaceholder

ExperimentalWhere a dragged item will land (design/patterns/drag-and-drop.md#where-it-will-land): a 1.5px dashed `line-strong` outline on `fill-subtle`, with the item's radius and size. One look for every place. The control radius is the default; pass the item's own (`rounded-container` for a card). Hidden from assistive technology: the announcements say where the item is.
PropTypeDefault
opaqueIn a layout (the gap a column makes, a new column's strip): an opaque `layer-canvas` under the `fill-subtle` wash, so the column behind the gap doesn't show through while its sections reflow. Nothing is drawn over live content (design/patterns/drag-and-drop.md#where-it-will-land).booleanfalse
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.ReactElement | ((props, state) => ReactElement)

Also takes the attributes of its element.

DragLiveRegion

ExperimentalA polite, visually hidden live region for drag announcements.
PropTypeDefault
message (required)string

DraggedSlot

ExperimentalWraps each item's content in a list. The dragged item's own node is at 40% while it is still home, and the dashed place (its size, its radius) once it stands anywhere else. Every other item renders as it is: the wrapper stays the same, so the item keeps focus when its drag starts.
PropTypeDefault
children (required)ReactNode
draggingThis item is the one being dragged. Render the slot around every item and pass `isDragging`, so the dragged one keeps its node (and focus).booleanfalse
home (required)The dragged item still stands at its own place.boolean
radiusThe item's radius class, for the place: `rounded-control` by default.string

Changes

  • 0.81.0 · FixedDragLift renders the copy in document.body by default: the lifted copy goes through a portal to the body unless container names another element, or null keeps it in place; the portal keeps the theme scope it was rendered in. TaskDragOverlay follows, as SortableTabsList and SortableComposerAttachments already did.
  • 0.81.0 · FixedIn a right-to-left layout the dragged copy keeps its direction: DragLift's portal carries the item's computed writing direction (dir) to the body, as it carries the theme scope.