Documentation
Guides

Drag and drop

How tabs are dragged and dropped, which command a drop runs, and how to style the drop indicator and the dragged tab.

Drag and drop works out of the box: every Dockable.Tab is draggable (when its tab's draggable allows it), and every tabset, tab strip and layout edge is a drop target. What you add is the look: the drop indicator, and the dragged tab's state.

Drag and drop
Gallery

What happens during a drag

  1. A tab uses native HTML5 drag and drop: draggable, dragstart, dragend. The browser snapshots the dragged Tab element as the drag image (no text is added). The drag carries the MIME type DRAG_TYPE (application/x-dockable), so a layout only claims its own drags.
  2. The engine listens for native dragenter/dragover/dragleave/drop on the root (not React events, so the same path works in a popout document) and hit-tests the pointer against the measured rects.
  3. Each candidate target becomes the command the drop would run, and the engine asks the model with model.can (a dry run: nothing changes). The first accepted candidate is the target, and the drop indicator state updates: where the tab would land and how.
  4. On drop, the engine runs that command with model.run: tab.move with { tabId, to, location, index } (or tabset.move for a tabset, tab.add for a new tab). A middleware sees it like any other command; veto it there and nothing moves.
drop onlocationresult
a tab stripcenterthe tab joins the tabset at the pointer's index
the middle of a tabset's contentcenterthe tab joins the tabset (appended)
a tabset's edgetop, bottom, start, endthe tabset splits; the tab gets a new tabset on that side
the layout's outer edgetop, bottom, start, end (kind: "edge")a new tabset along the whole edge

Tabsets can be dropped too (tabset.move), but the React primitives start drags only from tabs today. useDragNode(node) takes a tab or a tabset node and returns the props that wire an element to the drag machine, if you build your own tabset drag handle.

The drop indicator

Dockable.DropIndicator is one element the engine positions over the drop target (structurally: position: absolute, left/top/width/height, display: none while there is no target, and pointer-events: none). Place it anywhere inside the root and style it:

<Dockable.DropIndicator
    className="z-10 rounded-md border-2 border-palette-ring bg-palette-ring/15 data-[drop-kind=edge]:border-dashed"
    style={(state) => ({
        transitionProperty: "left, top, width, height",
        transitionDuration: `${state.tabDragSpeed}s`,
    })}
/>

Its state (also as attributes):

stateattributemeaning
visibledata-visiblea drop target is under the pointer
locationdata-drop-location (while visible)center, top, bottom, start or end
kinddata-drop-kind (while visible)edge for a layout-edge drop, rect otherwise
draggingdata-dragginga drag is over this layout
showEdgesnoneedge docking is available for this drag
tabDragSpeednoneseconds a style may take to animate between targets (tabDragSpeed on Root, default 0.3)
refuseddata-drop-refusedthe pointer is over a target the model refuses
targetNodeIdnonethe drop target node: a tabset, a row for an edge drop, a border
targetTabsetIdnonethe tabset (or border) the drop goes into or beside

The core never animates: the transition is yours, and tabDragSpeed only tells you the duration the layout was configured with.

Since className takes the state, the indicator can say where a tab would land. location and kind give the side and whether the drop is at the layout's edge, and targetTabsetId the tabset the drop goes into or beside (that tabset also has data-drop-target). The example gives each region its colour and each kind of drop its fill:

Drop indicator colours
Gallery

Stacking: give it a z-index

Panels are portalled into the root after its other children. Absolutely positioned siblings without a z-index paint in DOM order, so an indicator without one paints under the panels and is hidden over tab content. Give it a z-index (z-10 above). This is gap 12 in Limitations.

pointer-events: none is structural on purpose: an indicator under the pointer would take the drag's enter and leave events from the layout, and the browser would cancel the drop.

Styling the drag

  • The dragged tab has data-dragging while it is dragged: fade it (data-dragging:opacity-40).
  • The root has data-dragging while any node of the layout is dragged: for example to show drop hints or disable hover effects.
  • The drag image is the browser's snapshot of the Tab element, styled as it is when the drag starts.

Layout rules that affect drops

  • Keep tab strips at least ~30px tall, or lower edgeDockMargin. The layout's top edge has a 10px docking band (defaults.layout.edgeDockMargin); a thin strip at the top of the layout falls inside it, and drops aimed at the strip dock to the layout edge instead (gap 11). Dockable.EdgeIndicator shows the bands during a drag (Borders).
  • Edge docking can be turned off in the layout's defaults, defaults.layout.edgeDock: false (or at run time with layout.configure).

Dropping between windows and layouts

Tabs drag between the main window and its popouts, and between popouts, with no set-up. Two separate Dockable.Roots (different models) exchange tabs when a Dockable.DragGroup wraps them. See Drag across windows and layouts. Dragging things from outside the layout into it (a widget sidebar, files) is covered in External drag.

Controlling where drops go

Node flags (draggable, droppable, splittable, and pinned tabs) and a middleware on tab.move, tabset.move and tab.add decide what may be dropped where. The indicator refuses a target exactly when model.can refuses its command, so both show the same way; see Restricting drops.