Skip to contents

Overview

A guide to building drag-and-drop interfaces with Base UI.

Base UI includes headless drag-and-drop for making elements draggable and building drop zones. It works across mouse, touch, and pen, with pointer-appropriate defaults for starting a drag (see Activation). Draggable.Root also supports keyboard dragging by default (see Keyboard navigation). The components are unstyled, including the preview that follows the pointer.

The two main components have their own pages. Draggable makes an element a drag source, and DropTarget marks where a drag can be released. The other guides cover styling, accessibility, collections, and testing.

Concepts

Kinds say what can be dragged where. Every draggable is of one kind, declared once with Draggable.createKind. A drop target lists the kinds it takes in accept, and the payload type the kind was created with is what types source.payload on every event:

Declaring a kind

A createKind call has a unique identity. Declare it once and share the returned value with every draggable and target in the interaction. Two separate createKind('card') calls do not match. The name is only a debugging aid. Use label to set the accessible name of a draggable or drop target.

If independently evaluated bundles cannot share the same value, use Draggable.createGlobalKind<Card>('myapp/card'). Global keys are interned across bundles and hot reloads, so namespace them to your app or package. Reusing one key with incompatible payload types makes unrelated integrations match and bypasses TypeScript’s payload safety; prefer createKind everywhere else.

One manager, no provider. Every source and target on the page uses the same drag manager. The required accept prop prevents unrelated interactions from matching. Use DropTarget.anyKind to accept every drag. Draggable.PreviewProvider does not scope a drag. It provides the React tree where custom previews render.

Drag and drop is synthetic. Base UI tracks pointer and keyboard input itself rather than using the browser’s HTML5 drag-and-drop, so there is no dataTransfer, nothing crosses into other applications, and the preview is an ordinary element you style. An OS file drop is handled by passing native handlers through render.

Drop targets stack. A drag can be over several nested targets at once. They arrive innermost-first in location.current.dropTargets, and only the innermost receives onDrop; ancestors see the drag pass through via onDragEnter, onDrag and onDragLeave. Returning false from canDrop pops a target off the stack so an ancestor can claim the drop; returning 'reject' refuses the position outright, and no target resolves there at all.

A drop can resolve to a value inside a target. Every record in the stack has a getLocalPoint() method. It returns the pointer position inside the target as a fraction of its bounding box. Use it to resolve values such as a time in a day column or a position on a track without measuring the element in the handler.

Every event carries the same location history. location.initial is where the drag began, location.current where it is now, and location.previous where it was at the prior event, each a pointer position plus the target stack at that moment. Comparing current against previous is how hover work tells that something changed. On the first event of a drag, previous holds the pickup input and an empty stack, so that comparison reads as no movement rather than a jump.

Setup

You do not need a provider for the default clone or Draggable.ClonedPreview. When you render custom content with Draggable.Preview, wrap that part of your app in a Draggable.PreviewProvider. Put it inside the context providers the preview needs:

Setting up drag and drop

Custom preview content renders beside the provider’s children, so it receives context only from providers above the nearest Draggable.PreviewProvider. It does not inherit a theme, direction, or store provider placed between that preview provider and an individual draggable. Put another Draggable.PreviewProvider inside any local context boundary the preview must retain. The DOM container a preview is injected into does not change this React-context boundary.

Examples

Sortable lists

Do not merge: a List Box example will replace this one once that component is available.

To reorder a list, pass a DropTarget.Root to each item’s render prop, so both roles land on one element.

Draft the proposal
Review the budget
Email the client
Prepare the slides
Book the room

Draggable tabs

Reordering leaves the standard Tabs keyboard behavior in place. The arrow keys still move focus and select, Alt+/ reorders the focused tab, and Delete closes it.

Workspace

A clear view of the project

Keep notes, decisions, and next steps together in one shared place.

Calendar

A custom modifier snaps the drag to the nearest day column and 15-minute slot, so the preview shows exactly where the event will land. Each column declares snap, so the drop commits with no geometry in the handler.

Monday
Tuesday
Wednesday
9:0010:0011:0012:0013:00
Design review10:0011:00

Free dragging

Cards sit at absolute coordinates and drop anywhere. restrictToElement keeps the drag inside the canvas, and the drop commits the exact position where you release the preview.

Kanban board

useDragMonitor resolves the closest column and insertion slot on every drag event, and an empty placeholder card marks where the drop will land.

Todo
Write the spec
Sketch the UI
Set up the repo
In progress
Wire the API
Build the form
Done
Ship v0

File explorer

Drop a node on a folder, on the open grid, or on an ancestor in the breadcrumb to move it there. With a tile focused, press Alt+Enter to start a keyboard drag; plain Space or Enter opens a folder. canDrop refuses a folder dropped into its own subtree.

Archive
Documents
Music
Photos
Projects
budget.xlsx
cover-letter.pdf
notes.txt
report-q3.pdf
resume.pdf
slides.key
todo.md