drag-surface

Accessible drag-and-drop reorder surface with keyboard support, live region announcements, and cross-surface transfer.

Overview

The <drag-surface> component wraps the native HTML Drag and Drop API, adding keyboard accessibility, live region announcements for screen readers, and clean custom events. It supports both single-surface reorder and cross-surface transfer between surfaces that share a data-group attribute.

Children just need draggable="true" — the component handles all event wiring, state attributes for CSS, and ARIA management.

<drag-surface data-layout="stack" data-layout-gap="xs"> <article class="card" draggable="true" data-id="task-1">Design review</article> <article class="card" draggable="true" data-id="task-2">Code audit</article> <article class="card" draggable="true" data-id="task-3">Deploy staging</article> </drag-surface>

<drag-surface> uses flex: 1 to fill available space when placed inside a flex container (such as a kanban column using data-layout="stack"). This ensures the full column area is a valid drop target, not just the space occupied by cards.

Attributes

On <drag-surface>

AttributeTypeDefaultDescription
data-group string Transfer group. Items can move between surfaces that share the same group value.
data-orientation string "vertical" Set to "horizontal" to use Left/Right arrows and clientX for drop position.
data-drag-disabled boolean Disables all dragging on this surface.

On Children

AttributeTypeDescription
draggable="true" boolean Native HTML — marks the element as draggable. Required.
data-id string Stable identifier for the item (survives reorder).
data-sort-order number Numeric position within the surface. Managed automatically on reorder.
data-drag-handle boolean If present on a descendant, only that element initiates the drag.

CSS State Attributes

The component manages visual state entirely through data attributes, keeping all styling in CSS.

AttributeSet OnWhen
data-dragging The dragged child During a mouse drag operation
data-drag-over The surface A dragged item is hovering over a valid drop target
data-drop-target="before|after" A child item Shows where the dragged item will be inserted
data-reorder-mode The surface During keyboard reorder (item is grabbed)
aria-grabbed="true" A child item Item is grabbed for keyboard reorder
data-just-dropped A child item Briefly set after a drop (mouse or keyboard) to trigger a flash animation. Removed automatically after the animation ends.

Kanban Board

Link multiple surfaces with data-group to enable cross-surface transfer. Items can be dragged between any surfaces that share the same group value.

<section data-layout="grid" data-layout-min="15rem"> <section data-layout="stack" data-layout-gap="s"> <h3>To Do</h3> <drag-surface data-group="tasks" aria-label="To Do" data-layout="stack" data-layout-gap="xs"> <article class="card" draggable="true" data-id="task-1">Design review</article> <article class="card" draggable="true" data-id="task-2">Write docs</article> </drag-surface> </section> <section data-layout="stack" data-layout-gap="s"> <h3>In Progress</h3> <drag-surface data-group="tasks" aria-label="In Progress" data-layout="stack" data-layout-gap="xs"> <article class="card" draggable="true" data-id="task-3">API integration</article> </drag-surface> </section> <section data-layout="stack" data-layout-gap="s"> <h3>Done</h3> <drag-surface data-group="tasks" aria-label="Done" data-layout="stack" data-layout-gap="xs"> <!-- empty, ready to receive --> </drag-surface> </section> </section>

Drag Handles

Add data-drag-handle to a descendant element to constrain where the drag can be initiated. Only clicks on the handle start a drag — clicking other parts of the item does not.

<drag-surface data-layout="stack" data-layout-gap="xs"> <article class="card" draggable="true" data-id="item-1"> <span data-drag-handle style="cursor: grab;">&#x2630;</span> Item with drag handle </article> <article class="card" draggable="true" data-id="item-2"> <span data-drag-handle style="cursor: grab;">&#x2630;</span> Another item </article> </drag-surface>

Horizontal Layout

Set data-orientation="horizontal" to switch keyboard navigation to Left/Right arrows and use horizontal position for drop calculations.

<drag-surface data-orientation="horizontal" data-layout="cluster"> <article class="card" draggable="true" data-id="col-a">Column A</article> <article class="card" draggable="true" data-id="col-b">Column B</article> <article class="card" draggable="true" data-id="col-c">Column C</article> </drag-surface>

Events

EventDetailDescription
items-reordered {item, itemId, oldIndex, newIndex, order} Items within this surface were reordered (mouse or keyboard).
item-transferred {item, itemId, fromSurface, toSurface, newIndex, fromOrder, toOrder} An item moved between surfaces sharing a data-group.
reorder-start A drag or keyboard reorder began.
reorder-end A drag or keyboard reorder ended.
const surface = document.querySelector('drag-surface'); surface.addEventListener('items-reordered', (e) => { console.log(e.detail); // { // item: HTMLElement, // itemId: "task-2", // oldIndex: 2, // newIndex: 0, // order: ["task-2", "task-1", "task-3"] // } }); // Cross-surface transfer (listen on document or a parent) document.addEventListener('item-transferred', (e) => { console.log(e.detail); // { // item: HTMLElement, // itemId: "task-1", // fromSurface: HTMLElement, // toSurface: HTMLElement, // newIndex: 1, // fromOrder: ["task-2"], // toOrder: ["task-3", "task-1"] // } });

Keyboard Navigation

KeyAction
Space / Enter Grab the focused item (toggle). Press again to drop at current position.
ArrowUp / ArrowDown Move the grabbed item up/down (vertical orientation).
ArrowLeft / ArrowRight Move the grabbed item left/right (horizontal orientation), or transfer between grouped surfaces (vertical orientation).
Escape Cancel the reorder and return the item to its original position. Does not undo cross-surface transfers.

Keyboard Transfer

When surfaces share a data-group, grabbed items can move between them using the perpendicular arrow keys:

  • Vertical surfaces (default): ArrowLeft / ArrowRight transfer to the adjacent surface
  • Horizontal surfaces: ArrowUp / ArrowDown transfer to the adjacent surface

Adjacent surfaces are determined by visual position (left-to-right, top-to-bottom). The transfer is committed immediately — pressing Escape afterwards will not undo a cross-surface transfer, only cancel further reordering within the new surface.

After transfer, the item receives focus in the new surface and a data-just-dropped flash animation plays. The item-transferred event fires on the receiving surface.

Disabled State

Add data-drag-disabled to the surface to temporarily prevent all dragging.

<!-- Disable the entire surface --> <drag-surface data-drag-disabled data-layout="stack" data-layout-gap="xs"> <article class="card" draggable="true" data-id="a">Can't drag me</article> <article class="card" draggable="true" data-id="b">Or me</article> </drag-surface>

Accessibility

ARIA Roles

The surface sets role="list" on itself and ensures each draggable child has role="listitem", tabindex="0", and aria-grabbed="false".

Live Region

A visually hidden aria-live="polite" region announces position changes during keyboard reorder (e.g., “Position 2 of 5”) and grab/drop actions.

Keyboard Alternative

The keyboard reorder mode is the accessible alternative to mouse drag-and-drop. Users can grab items with Space/Enter, move with arrow keys, drop with Space/Enter, and cancel with Escape.

Reduced Motion

When prefers-reduced-motion: reduce is active, the component skips any transition animations.

Progressive Enhancement

LayerWhat WorksWhat’s Missing
HTML only Content is readable as a static list No interactivity
HTML + draggable Browser provides native drag ghost No drop handling, no keyboard support
HTML + <drag-surface> JS Full reorder, transfer, keyboard, live announcements No persistence (resets on reload)
HTML + JS + consumer code Consumer listens to events, persists order Full experience

Related Elements