Sortable List
import { SortableList } from '@absoluteui/components/sortable-list';import '@absoluteui/components/sortable-list/style.css';Using Angular? See Sortable List for Angular.
What it does
Section titled “What it does”A labelled, vertical list that users reorder. Two equivalent input paths:
- Pointer drag through pointer events (mouse, pen, touch), from the handle or, with
dragFrom: 'item', from anywhere on the item. HTML5 drag and drop is never used. - Keyboard on each item’s handle button: Space or Enter grabs, arrow keys, Home, and End move, Space or Enter drops, Escape or leaving the handle cancels.
Every step is announced through the assertive live region. The moved item never leaves the DOM (its siblings move around it), so focus and pointer capture survive the reorder. The new order is reported with aui-change; persisting it is the consumer’s job.
When to use
Section titled “When to use”- Letting users set the order of a short list, such as priorities, playlist tracks, or dashboard widgets.
- Ordering that must be operable by keyboard, pointer, and touch alike.
When not to use
Section titled “When not to use”- Sorting data by a column value: use Data Table sorting.
- Moving items between two lists (for example, available and selected): use a transfer list pattern; drag between lists is not supported.
- Grids or horizontal layouts: only vertical lists are supported.
- Very long lists where users must scroll while dragging: the list does not auto-scroll; offer move-to-position controls instead.
- Choosing one item: use a radio group or listbox.
Examples
Section titled “Examples”Reorder priorities and keep the options in sync
Section titled “Reorder priorities and keep the options in sync”import { SortableList, type SortableListChangeDetail, type SortableListItem } from '@absoluteui/components/sortable-list';
const host = document.querySelector<HTMLElement>('#priorities');if (host) { const list = SortableList.create(host, { label: 'Priorities', items: [ { id: 'security', label: 'Security review' }, { id: 'performance', label: 'Performance' }, { id: 'docs', label: 'Documentation' }, ], }); host.addEventListener('aui-change', (event) => { const { order } = (event as CustomEvent<SortableListChangeDetail>).detail; const byId = new Map(list.options.items.map((item): [string, SortableListItem] => [item.id, item])); // Feed the new order back so later updates and config changes keep it. list.update({ items: order.flatMap((id) => byId.get(id) ?? []) }); // persist `order` here });}Rich item content, a pinned item, and dragging from the whole item
Section titled “Rich item content, a pinned item, and dragging from the whole item”import { SortableList, type SortableListItem } from '@absoluteui/components/sortable-list';
const card = (title: string, owner: string): HTMLElement => { const el = document.createElement('div'); const strong = document.createElement('strong'); strong.textContent = title; el.append(strong, ` - ${owner}`); return el;};const items: SortableListItem[] = [ { id: 'notes', label: 'Release notes', content: card('Release notes', 'Ana'), disabled: true }, { id: 'api', label: 'API review', content: card('API review', 'Ben') }, { id: 'qa', label: 'QA pass', content: card('QA pass', 'Chen') },];const host = document.createElement('div');document.body.append(host);SortableList.create(host, { label: 'Backlog', items, dragFrom: 'item' });Programmatic move-to-top control
Section titled “Programmatic move-to-top control”import { SortableList } from '@absoluteui/components/sortable-list';
const host = document.createElement('div');document.body.append(host);const tracks = SortableList.create(host, { label: 'Playlist', items: [ { id: 't1', label: 'Intro' }, { id: 't2', label: 'Theme' }, { id: 't3', label: 'Finale' }, ],});document.querySelector('#finale-first')?.addEventListener('click', () => { if (!tracks.isDragging) tracks.move('t3', 0); const order: string[] = tracks.order; host.dataset.order = order.join(',');});import { SortableList, type SortableListChangeDetail } from '@absoluteui/components/sortable-list';import '@absoluteui/components/sortable-list/style.css';
const host = document.querySelector<HTMLElement>('#priorities');if (host) { SortableList.create(host, { label: 'Priorities', items: [ { id: 'security', label: 'Security review' }, { id: 'performance', label: 'Performance' }, ], }); host.addEventListener('aui-change', (event) => { const { order } = (event as CustomEvent<SortableListChangeDetail>).detail; // persist `order` });}More usage
Section titled “More usage”import { SortableList } from '@absoluteui/components/sortable-list';
const host = document.createElement('div');document.body.append(host);SortableList.create(host, { label: 'Priorities', items: [ { id: 'security', label: 'Security review' }, { id: 'performance', label: 'Performance' }, { id: 'docs', label: 'Documentation', disabled: true }, ],});Anatomy
Section titled “Anatomy”<div class="aui-sortable-list [aui-sortable-list--disabled] [aui-sortable-list--drag-item]"> (consumer host) <ul class="aui-sortable-list__list" aria-label="{label}"> <li class="aui-sortable-list__item [--grabbed] [--dragging] [--disabled]" data-id="{id}" style="--aui-sortable-list-offset (CSSOM, pointer drag only)"> <button type="button" class="aui-sortable-list__handle" aria-label="Reorder {label}" aria-pressed="false|true" aria-describedby="{instructions id}" disabled?> <svg class="aui-icon" aria-hidden="true"> (icon 'menu') </button> <div class="aui-sortable-list__content">content or label text</div> </li> ... </ul> <p class="aui-sortable-list__instructions" id>visually hidden instructions</p></div>API reference
Section titled “API reference”Factory
Section titled “Factory”SortableList.create(host: HTMLElement, options: SortableListOptions): SortableListGlobal configuration key: sortableList (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Accessible name of the list (aria-label on the <ul>). Must be non-empty after trimming. Required. | |
items |
readonly { id: string; label: string; content?: Node | string; disabled?: boolean }[] |
Items in order. Ids must be unique, non-empty, and stable; labels non-empty (used in the handle name and announcements). Rows are kept by id on update; the array order defines the DOM order. Required. | |
disabled |
boolean |
false |
Disable reordering for the whole list (all handles disabled). |
icons |
Partial<{ handle: IconRef }> |
Icon slot overrides. | |
dragFrom |
'handle' | 'item' |
"handle" |
Where a pointer drag can start: the handle only, or anywhere on a movable item (interactive content inside the item keeps its own pointer behavior). Globally configurable. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Consumer host; its children are owned by the component. |
list |
HTMLUListElement |
The <ul> element. |
order |
string[] |
Item ids in the current DOM order. |
isDragging |
boolean |
Whether an item is grabbed (keyboard) or dragged (pointer). |
options |
Readonly<SortableListOptions> |
Current instance options (items keep the last order passed in, not the DOM order). |
Methods
Section titled “Methods”| Method | Description |
|---|---|
move(id: string, index: number): void |
Move an item to a zero-based index; cancels an active grab or drag first. No event, no announcement. Throws SORTABLE_LIST_OPTIONS for an unknown id or an index outside 0..count-1. |
update(options: Partial<SortableListOptions>): void |
Merge, validate, and apply options; cancels an active grab or drag. Rows and content nodes are kept by id; content is re-inserted only when it changed. |
destroy(): void |
Cancel any grab or drag, remove rendering, listeners, drag tracking, and subscriptions, and clear the host’s children and classes. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-change |
{ order: string[]; id: string; from: number; to: number } |
Yes | No | Dispatched on the host after a keyboard or pointer drop that changed the item’s position. from and to are zero-based. Not emitted for cancels, drops in place, move(), or update(). |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
item content |
Each item’s content (Node or text) or, without it, its label text, inside .aui-sortable-list__content. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-sortable-list |
Host. |
aui-sortable-list--disabled |
Host modifier: the whole list is disabled. |
aui-sortable-list--drag-item |
Host modifier: dragFrom is ‘item’. |
aui-sortable-list__list |
The <ul> (grid with space-2 gap). |
aui-sortable-list__item |
One <li> per item; data-id holds the item id. |
aui-sortable-list__item--grabbed |
The grabbed or dragged item. |
aui-sortable-list__item--dragging |
The item is following the pointer (translateY by –aui-sortable-list-offset). |
aui-sortable-list__item--disabled |
The item cannot be moved. |
aui-sortable-list__handle |
Handle <button> with the grip icon. |
aui-sortable-list__content |
Item content wrapper. |
aui-sortable-list__instructions |
Visually hidden instructions referenced by every handle’s aria-describedby. |
--aui-sortable-list-offset |
Custom property set through the CSSOM during a pointer drag: vertical offset in px. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
handle |
menu |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-border-width | --aui-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-sm | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-icon-size-md | --aui-line-height-normal | --aui-radius-md | --aui-radius-sm | --aui-shadow-lg | --aui-space-1 | --aui-space-2 | --aui-space-3
TypeScript definitions
Section titled “TypeScript definitions”type SortableListDragFrom = 'handle' | 'item';interface SortableListConfig { dragFrom: SortableListDragFrom }interface SortableListItem { id: string; label: string; content?: Node | string; disabled?: boolean }interface SortableListIcons { handle: IconRef }interface SortableListOptions extends Partial<SortableListConfig> { label: string; items: readonly SortableListItem[]; disabled?: boolean; icons?: Partial<SortableListIcons>;}interface SortableListChangeDetail { order: string[]; id: string; from: number; to: number }Accessibility
Section titled “Accessibility”Pattern: Reorderable list with a toggle-button grab handle per item and live-region announcements (keyboard drag-and-drop pattern). Native list semantics; no ARIA drag-and-drop attributes (aria-grabbed and aria-dropeffect are deprecated).
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Not grabbed | Moves between handle buttons (one per enabled item) and interactive item content in document order. |
| Space / Enter | Handle focused, nothing grabbed, item movable | Grabs the item (aria-pressed=true); announces ‘Grabbed {label}. Current position {position} of {count}.’ |
| Space / Enter | Grabbed item’s handle focused | Drops the item; announces ‘Dropped {label} at position {position} of {count}.’; emits aui-change when the position changed. |
| ArrowUp / ArrowLeft | Item grabbed | Moves the item one position toward the start (stops at the first); announces ‘{label} moved to position {position} of {count}.’ |
| ArrowDown / ArrowRight | Item grabbed | Moves the item one position toward the end (stops at the last); announces the new position. |
| Home / End | Item grabbed | Moves the item to the first / last position. |
| Escape | Item grabbed | Cancels: the item returns to its original position; announces ‘Reorder canceled. {label} returned to position {position} of {count}.’ Consumed before the overlay stack, so an enclosing dialog stays open. |
| Escape | Pointer drag in progress | Cancels the drag and restores the original order; consumed before the overlay stack. |
| Tab / Shift+Tab | Item grabbed | Focus leaves the handle, which cancels the reorder (announced). |
| Other keys | Item grabbed | Swallowed (preventDefault) so they do not scroll or type. Keys with Alt, Ctrl, or Meta are ignored by the component. |
| Element | Role |
|---|---|
| <ul> | list (native) |
| <li> | listitem (native) |
| handle <button> | button (native, toggle through aria-pressed) |
| instructions <p> | none (visually hidden description) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-label |
<ul> | label option |
aria-label |
handle | Localized ‘Reorder {label}’ |
aria-pressed |
handle | true while the item is grabbed or dragged, false otherwise |
aria-describedby |
handle | id of the visually hidden instructions paragraph |
disabled |
handle | Native disabled for disabled items or a disabled list (removed from the tab order) |
live region |
shared assertive live region (data-aui-live-region=‘assertive’) | Grabbed / moved / dropped / canceled announcements |
Focus stays on the grabbed item’s handle throughout a keyboard move because the item element never leaves the DOM (its siblings are moved around it). Leaving the handle (Tab, click elsewhere, window blur) cancels the reorder. Disabled handles are not focusable.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”| Criterion | Level | Note |
|---|---|---|
| 1.3.1 Info and Relationships | A | |
| 2.1.1 Keyboard | A | |
| 2.4.3 Focus Order | A | |
| 2.4.7 Focus Visible | AA | |
| 2.5.7 Dragging Movements | AA | |
| 4.1.2 Name, Role, Value | A | |
| 4.1.3 Status Messages | AA |
Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- Dragging is never the only way to reorder (WCAG 2.5.7): the keyboard path on the handle button works with a single pointer too (activate the handle, then use keys), and move() supports custom move-to controls.
- The grabbed state is conveyed by aria-pressed, a selected background, and a border (not color alone); forced-colors mode uses Highlight.
- Instructions are exposed as the handle’s accessible description, not repeated in every announcement.
- Announcements go through the shared assertive live region, which follows an open modal dialog so messages inside dialogs are not lost.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
- Some screen readers intercept Space, Enter, and arrow keys in browse mode; users may need focus/forms mode on the handle.
- Pointer drags announce every position change assertively, which can be verbose.
Behavior
Section titled “Behavior”States
Section titled “States”idle, handle hover, handle focus-visible, grabbed (keyboard), dragging (pointer), item disabled, list disabled.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| idle | Space / Enter on a movable item’s handle | grabbed (announced “Grabbed …”) |
| grabbed | Arrow / Home / End | grabbed at the new position (announced “… moved to position …”) |
| grabbed | Space / Enter | idle (announced “Dropped …”; aui-change when the position changed) |
| grabbed | Escape, or focus leaves the handle | idle at the original position (announced “Reorder canceled …”) |
| idle | pointerdown on the handle (or item with dragFrom: 'item'), then a move of more than 4 px |
dragging (announced “Grabbed …”) |
| dragging | the item’s middle crosses a sibling’s middle | dragging at the new position (announced) |
| dragging | pointer release | idle (announced “Dropped …”; aui-change when the position changed) |
| dragging | Escape, pointercancel, lost pointer capture |
idle at the original position (announced “Reorder canceled …”) |
| grabbed / dragging | move(), update(), destroy() |
idle; the item returns to its original position first (no announcement) |
Accessibility notes
Section titled “Accessibility notes”- Focus stays on the grabbed handle during a keyboard move because the item element never leaves the DOM.
- Dragging is not required (WCAG 2.5.7): a single-pointer user can activate the handle and use the keys, or the consumer can offer
move()-based controls. - The grabbed state is conveyed by
aria-pressed, a selected background, and a border, not color alone. Forced-colors mode usesHighlight/HighlightText. - The live region follows an open modal dialog, so announcements inside dialogs are not lost.
- Messages are registered under
sortableList(instructions,handle,grabbed,moved,dropped,canceled) and can be localized. - Manual screen-reader verification is pending.
Responsive behavior
Section titled “Responsive behavior”Items fill the list width; content wraps with overflow-wrap: anywhere. The handle is a control-height-sm square with touch-action: none, so touch drags from the handle never scroll the page, while touching item content still scrolls. With dragFrom: 'item', movable items also get touch-action: none: the page cannot be scrolled by touching them. During a pointer drag the item is clamped to the list’s visible bounds; there is no auto-scroll.
Right-to-left
Section titled “Right-to-left”Logical properties throughout; the handle sits at the inline start. The list is vertical, so direction does not change reordering: ArrowUp and ArrowLeft always move toward the start of the list in both LTR and RTL.
Theming
Section titled “Theming”Token-driven item surface, border, and radius; color-selected/color-selected-text for the grabbed item; shadow-lg while following the pointer; color-hover on the handle; color-text-disabled for disabled handles and color-text-muted for disabled item content. Forced-colors mode uses CanvasText borders, Highlight for the grabbed item, and GrayText for disabled handles.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { sortableList: { dragFrom: 'item' } } });Globally configurable: dragFrom. Changes apply to existing instances immediately.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic.
icons.handle (default menu), resolved through the global icon provider/registry. The icon is decorative; the handle’s name comes from aria-label.
Security
Section titled “Security”The list label, item labels, announcements, and string content use textContent. No HTML parsing. The drag offset is written as a CSS custom property through the CSSOM, so the component is CSP-safe.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. aui-change reports the new order; the consumer stores it.
Performance
Section titled “Performance”Two listeners on the list (keydown, focusout), one pointer-drag tracker, and one configuration subscription per instance. A keyboard move moves the item by moving its siblings (one DOM move per step). During a pointer drag each move reads the bounding boxes of the list, the item, and the siblings it crosses. Re-renders reuse rows by id and replace the list’s children only when the order differs. Budgets in headless Chromium: SortableList.create with 500 items under 100 ms; one keyboard move with 500 items under 16 ms.
Supported data scale
Section titled “Supported data scale”Budgeted with 500 items. Because there is no auto-scroll, pointer reordering is practical only within the visible part of the list; for long lists rely on the keyboard path or move() controls. No virtualization.
Edge cases
Section titled “Edge cases”- Dropping at the original position announces “Dropped …” but emits no
aui-change. - A disabled item cannot be grabbed or dragged; other items move past it.
- With
dragFrom: 'item', a press on interactive content in the item (a,button,input,select,textarea,label,[contenteditable]) does not start a drag. - A press without movement (under 4 px) is a click, not a drag; the click after a completed drag is suppressed.
- A pointer drag cannot start while a keyboard grab is active.
- Only the primary pointer and, for mouse, the main button start a drag.
- A drop or
move()also reorders theitemsoption (options.items), so later re-renders keep the new order.update({ items })with an older order restores that order: pass the order fromaui-changeback (frameworks with bound inputs must). - A re-render that changes the order keeps focus on the focused handle.
- A global configuration change cancels an active grab or drag.
- An empty
itemsarray renders an empty, labelled list.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
SORTABLE_LIST_OPTIONS |
configuration | Blank label; items not an array; an item that is not an object; empty or duplicate id; blank item label; content that is not a Node or string; non-boolean disabled; move() with an unknown id or an out-of-range index. |
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION |
configuration | Invalid or unknown configuration value, such as dragFrom: 'anywhere'. |
A failed construction leaves nothing behind: the host’s children and classes are untouched and no listeners or subscriptions remain. A failed update() keeps the previous options and rendering.
Known limitations
Section titled “Known limitations”- Vertical lists only.
- No auto-scroll during a pointer drag.
- No drag between lists, no nesting, no multi-select.
update({ items })with a stale order undoes the user’s reorder; feed the order fromaui-changeback.- Pointer drags announce every position change, which can be verbose.
- Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- HTML5 drag and drop (
draggable,dataTransfer), including dropping files or external data. - Horizontal or grid layouts.
- Dragging between lists or into nested lists.
- HTML strings as labels or content: pass a
Nodeinstead.