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> 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>
| Attribute | Type | Default | Description |
|---|---|---|---|
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
| Attribute | Type | Description |
|---|---|---|
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.
| Attribute | Set On | When |
|---|---|---|
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.
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.
Horizontal Layout
Set data-orientation="horizontal" to switch keyboard navigation to Left/Right arrows and use horizontal position for drop calculations.
Events
| Event | Detail | Description |
|---|---|---|
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. |
Keyboard Navigation
| Key | Action |
|---|---|
| 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.
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
| Layer | What Works | What’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
draggable— the native HTML attribute that makes elements draggable<slide-accept-wc>— slide-to-confirm interaction