Dashboard Grid
import { DashboardGrid, DashboardCatalog, type DashboardItem } from '@absoluteui/components/dashboard-grid';import '@absoluteui/components/dashboard-grid/style.css';Using Angular? See Dashboard Grid for Angular.
What it does
Section titled “What it does”A responsive, user-arrangeable dashboard layout. DashboardGrid enhances a host whose children carry data-aui-item="<id>" into a labelled region laid out as a CSS grid in column/row units. In edit mode every item gets a move handle: drag it (mouse, pen, touch), or press it and use the arrow keys (Shift+arrows resize); a corner grip resizes by pointer. Other items are pushed down and float back up (vertical compaction); locked items never move. Layouts are stored per breakpoint, derived for narrower breakpoints when not saved, serializable, and undoable. The DOM order follows the visual reading order after every change, so focus order matches what users see.
The same subpath exports DashboardCatalog (a list of widget templates that adds widgets to a grid) and the pure layout functions the grid uses (collides, clampItem, sortLayout, compactLayout, resolveLayout, findFreeSpot, adaptLayout).
When to use
Section titled “When to use”- User-arrangeable dashboards of widgets (KPI cards, charts, tables) whose layout is saved per user.
- Fixed, responsive dashboard layouts defined in column/row units that must adapt to narrower screens.
- Letting users add widgets from a catalog of templates (DashboardCatalog).
When not to use
Section titled “When not to use”- Simple responsive page layout without user rearrangement: use CSS grid or the Layout component.
- Reordering a single vertical list: use Sortable List.
- Free-form canvases with pixel positioning, overlapping items, or rotation.
Examples
Section titled “Examples”A responsive dashboard from existing children
Section titled “A responsive dashboard from existing children”import { DashboardGrid, type DashboardItem } from '@absoluteui/components/dashboard-grid';
const host = document.createElement('div');for (const [id, title] of [['sales', 'Sales'], ['users', 'Active users'], ['map', 'Regions']] as const) { const section = document.createElement('section'); section.dataset.auiItem = id; const heading = document.createElement('h2'); heading.textContent = title; section.append(heading); host.append(section);}document.body.append(host);
const items: DashboardItem[] = [ { id: 'sales', x: 0, y: 0, w: 6, h: 2 }, { id: 'users', x: 6, y: 0, w: 6, h: 2 }, { id: 'map', x: 0, y: 2, w: 12, h: 3, minH: 2 },];const grid = DashboardGrid.create(host, { label: 'Overview', items });host.dataset.breakpoint = grid.breakpoint.name;Edit mode, saving layouts per breakpoint, and undo
Section titled “Edit mode, saving layouts per breakpoint, and undo”import { DashboardGrid, type DashboardGridLayoutChangeDetail, type DashboardItem } from '@absoluteui/components/dashboard-grid';import { Button } from '@absoluteui/components/button';
const items: DashboardItem[] = [ { id: 'kpi', x: 0, y: 0, w: 4, h: 2, label: 'Revenue' }, { id: 'chart', x: 4, y: 0, w: 8, h: 4, minW: 4 }, { id: 'notice', x: 0, y: 2, w: 4, h: 2, locked: true },];let saved: Record<string, DashboardItem[]> = {};
const host = document.querySelector<HTMLElement>('#dashboard');if (host) { const grid = DashboardGrid.create(host, { label: 'Operations', items, layouts: saved, editable: true, rowHeight: 96 }); host.addEventListener('aui-layout-change', (event) => { const { layouts } = (event as CustomEvent<DashboardGridLayoutChangeDetail>).detail; saved = layouts; }); const undo = document.createElement('button'); undo.type = 'button'; undo.textContent = 'Undo layout change';Button.enhance(undo); undo.addEventListener('click', () => { grid.undo(); }); const done = document.createElement('button'); done.type = 'button'; done.textContent = 'Done editing';Button.enhance(done, { variant: 'primary' }); done.addEventListener('click', () => { grid.setEditable(false); }); host.before(undo, done);}Add widgets from a catalog (click, Enter, Space, or drag onto the grid)
Section titled “Add widgets from a catalog (click, Enter, Space, or drag onto the grid)”import { DashboardCatalog, DashboardGrid, type DashboardCatalogAddDetail, type DashboardTemplate } from '@absoluteui/components/dashboard-grid';import { DashboardWidget } from '@absoluteui/components/dashboard-widget';
const catalogHost = document.createElement('div');const gridHost = document.createElement('div');document.body.append(catalogHost, gridHost);
const grid = DashboardGrid.create(gridHost, { label: 'My dashboard', items: [], editable: true });const templates: DashboardTemplate[] = [ { id: 'kpi', label: 'KPI', description: 'A single metric', w: 3, h: 2 }, { id: 'chart', label: 'Chart', description: 'A trend over time', w: 6, h: 4, minW: 4 },];DashboardCatalog.create(catalogHost, { grid, label: 'Widgets', templates, create: (template, item) => { const section = document.createElement('section'); DashboardWidget.create(section, { title: template.label, removable: true }); section.addEventListener('aui-remove', () => { grid.removeItem(item.id); }); return section; },});// At most four charts: veto further additions.catalogHost.addEventListener('aui-catalog-add', (event) => { const { template } = (event as CustomEvent<DashboardCatalogAddDetail>).detail; if (template.id === 'chart' && grid.items.filter((i) => i.id.startsWith('chart-')).length >= 4) event.preventDefault();});Pure layout functions (no DOM)
Section titled “Pure layout functions (no DOM)”import { adaptLayout, compactLayout, findFreeSpot, resolveLayout, sortLayout, type DashboardItem } from '@absoluteui/components/dashboard-grid';
const layout: DashboardItem[] = [ { id: 'a', x: 0, y: 0, w: 6, h: 2 }, { id: 'b', x: 6, y: 0, w: 6, h: 2 }, { id: 'c', x: 0, y: 2, w: 12, h: 1 },];// Move c to the top-left at half width: a is pushed down, then the layout is compacted.const moved = compactLayout(resolveLayout(layout, { id: 'c', x: 0, y: 0, w: 6, h: 1 }, 12));const spot = findFreeSpot(moved, 4, 1, 12);const phone = adaptLayout(moved, 12, 1);const readingOrder = sortLayout(phone).map((item) => item.id);document.body.dataset.summary = `${String(spot.x)},${String(spot.y)} ${readingOrder.join(' ')}`;Taller rows, wider gaps, and free placement for every grid
Section titled “Taller rows, wider gaps, and free placement for every grid”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { dashboardGrid: { rowHeight: 96, gap: 24, compaction: 'none' } } });import { DashboardCatalog, DashboardGrid, type DashboardGridLayoutChangeDetail, type DashboardItem } from '@absoluteui/components/dashboard-grid';import '@absoluteui/components/dashboard-grid/style.css';
const gridHost = document.querySelector<HTMLElement>('#dashboard');const catalogHost = document.querySelector<HTMLElement>('#widgets');const items: DashboardItem[] = [{ id: 'sales', x: 0, y: 0, w: 6, h: 2 }];if (gridHost && catalogHost) { const grid = DashboardGrid.create(gridHost, { label: 'Overview', items, editable: true }); gridHost.addEventListener('aui-layout-change', (event) => { const { layouts } = (event as CustomEvent<DashboardGridLayoutChangeDetail>).detail; sessionStorage.setItem('dashboard', JSON.stringify(layouts)); }); DashboardCatalog.create(catalogHost, { grid, label: 'Widgets', templates: [{ id: 'note', label: 'Note', w: 3, h: 2 }], create: (template) => { const el = document.createElement('section'); el.textContent = template.label; return el; }, });}More usage
Section titled “More usage”import { DashboardGrid, type DashboardItem } from '@absoluteui/components/dashboard-grid';
const host = document.querySelector<HTMLElement>('#dashboard');const items: DashboardItem[] = [ { id: 'sales', x: 0, y: 0, w: 6, h: 2 }, { id: 'users', x: 6, y: 0, w: 6, h: 2 },];if (host) DashboardGrid.create(host, { label: 'Overview', items, editable: true });More examples (catalog, saving layouts, layout functions), type-checked in CI: metadata/dashboard-grid/examples.json.
Anatomy
Section titled “Anatomy”<div class="aui-dashboard [aui-dashboard--editing]" role="region" aria-label="{label}"> (consumer host) <section data-aui-item="sales" class="aui-dashboard__item [--locked] [--dragging]"> (consumer child; grid-column / grid-row via CSSOM) <button class="aui-dashboard__handle" aria-pressed="false" aria-label="Move Sales"> (edit mode; prepended) ...consumer content... <div class="aui-dashboard__resize" aria-hidden="true"> (edit mode; appended; pointer only) </section> <div class="aui-dashboard__placeholder" aria-hidden="true"> (while moving / previewing)</div>
<div class="aui-dashboard-catalog"> (catalog host; children replaced) <ul class="aui-dashboard-catalog__list" aria-label="{label}"> <li class="aui-dashboard-catalog__entry"> <button class="aui-dashboard-catalog__template" data-template="{id}" aria-label="Add {label}"> [icon.aui-dashboard-catalog__icon] <span class="aui-dashboard-catalog__text"><span class="…__label"> <span class="…__description"></span>API reference
Section titled “API reference”Factory
Section titled “Factory”DashboardGrid.create(host: HTMLElement, options: DashboardGridOptions): DashboardGrid; DashboardCatalog.create(host: HTMLElement, options: DashboardCatalogOptions): DashboardCatalog. Pure layout functions: collides(a: DashboardItem, b: DashboardItem): boolean; clampItem(item: DashboardItem, columns: number): DashboardItem; sortLayout<T extends Pick<DashboardItem, 'x' | 'y'>>(items: readonly T[]): T[]; compactLayout(items: readonly DashboardItem[], anchored?: ReadonlySet<string>): DashboardItem[]; resolveLayout(items: readonly DashboardItem[], moved: DashboardItem, columns: number, compaction?: DashboardCompaction): DashboardItem[]; findFreeSpot(items: readonly DashboardItem[], w: number, h: number, columns: number): { x: number; y: number }; adaptLayout(items: readonly DashboardItem[], fromColumns: number, columns: number): DashboardItem[]Global configuration key: dashboardGrid (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Accessible name of the dashboard region (aria-label on the host). Non-empty. Required. | |
items |
readonly DashboardItem[] |
Layout for the widest breakpoint: { id, x, y, w, h, minW?, minH?, maxW?, maxH?, locked?, label? } in column/row units (x, y integers >= 0; w, h integers >= 1; unique ids matching data-aui-item). Clamped to the column count. Narrower breakpoints derive from it unless layouts has one. Changing it with update() replaces every stored layout. Required. |
|
layouts |
Readonly<Record<string, readonly DashboardItem[]>> |
Saved layouts by breakpoint name (from the layouts getter or aui-layout-change detail.layouts). Entries for unknown breakpoint names are ignored; a layout for the widest breakpoint overrides items. |
|
breakpoints |
readonly DashboardBreakpoint[] |
[{"name":"lg","minWidth":1024,"columns":12},{"name":"md","minWidth":640,"columns":6},{"name":"sm","minWidth":0,"columns":1}] |
{ name, minWidth (CSS px of the host width, >= 0), columns (integer 1..48) }; unique names; one breakpoint must have minWidth 0. |
editable |
boolean |
false |
Edit mode: move handles, resize grips, keyboard moving, and undo/redo shortcuts. |
historyLimit |
number |
50 |
Undo depth, integer 0..500 (0 disables history). |
reorderDom |
boolean |
true |
Keep the item elements in visual reading order (row, then column) in the DOM, so focus order matches the layout. Focus is kept across reordering. |
rowHeight |
number |
80 |
Row height in CSS px, integer 24..600. Globally configurable. |
gap |
number |
16 |
Gap between items in CSS px, integer 0..64. Globally configurable. |
compaction |
'vertical' | 'none' |
"vertical" |
‘vertical’: items float up into free space (an item cannot rest below empty space); ‘none’: items stay where they are put. Globally configurable. |
locale |
string |
"en-US" |
Locale of the handle names and announcements. Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
rtl mirrors ArrowLeft/ArrowRight moves and pointer column mapping. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
The enhanced host (role=region). |
breakpoint |
DashboardBreakpoint |
Active breakpoint (a copy). |
items |
DashboardItem[] |
Items of the active breakpoint (copies; derived when no layout is stored for it). |
layouts |
Record<string, DashboardItem[]> |
Every stored layout by breakpoint name (copies); the widest breakpoint is always included. Serializable (JSON). |
editable |
boolean |
Edit mode. |
canUndo | canRedo |
boolean |
Undo / redo history is available. |
previewCell |
{ x: number; y: number } | null |
Last drop-preview cell of an external drag (the catalog), or null. |
DashboardCatalog.host |
HTMLElement |
The catalog host (class aui-dashboard-catalog). |
Methods
Section titled “Methods”| Method | Description |
|---|---|
update(options: Partial<DashboardGridOptions>): void |
Merge and apply options (validated first; cancels a grab). items, layouts, or breakpoints replace all stored layouts. No event. |
setEditable(editable: boolean): void |
Enter or leave edit mode (update({ editable })). |
addItem(item: Omit<DashboardItem, 'x' | 'y'> & Partial<Pick<DashboardItem, 'x' | 'y'>>, element?: HTMLElement): DashboardItem |
Add an item to every stored layout: at x/y in the active breakpoint (colliding items are pushed down), or the first free spot without a position; other stored layouts get their first free spot. element is given data-aui-item and appended; otherwise the child with the matching data-aui-item is used when it appears (frameworks render it). Returns the placed item; announces it; emits aui-layout-change (reason ‘add’). Throws for a duplicate id. |
removeItem(id: string, removeElement?: boolean): void |
Remove an item from every layout (compacting with vertical compaction); its element is removed unless removeElement is false (default true). Announces and emits aui-layout-change (reason ‘remove’). Unknown ids are ignored. |
setLayout(items: readonly DashboardItem[]): void |
Replace the active breakpoint’s layout (validated, clamped, compacted with vertical compaction). Undoable; emits aui-layout-change (reason ‘set’). |
undo(): boolean; redo(): boolean |
Restore the previous / next layout geometry (items added or removed since keep their state). Returns false when there is nothing to undo/redo. Announces and emits aui-layout-change (reason ‘undo’ / ‘redo’). |
previewAt(clientX: number, clientY: number, w: number, h: number): { x: number; y: number } | null |
Show a drop placeholder for an external drag at client coordinates and return its cell, or null (and clear) outside the grid. Used by DashboardCatalog. |
clearPreview(): void |
Remove the drop placeholder. |
handleFor(id: string): HTMLButtonElement | null |
The move handle of an item (edit mode only), for focusing. |
destroy(): void |
Remove handles, grips, placeholder, classes, placement styles, custom properties, role, aria-label, observers, and listeners. Children stay. |
update(options: Partial<DashboardCatalogOptions>): void |
Merge, validate, and re-render the template list. |
add(templateId: string, at?: { x: number; y: number }): DashboardItem | null |
Add a widget from a template (id ‘<template id>-<n>’, the template’s w/h/minW/minH and label) at at or the first free spot. Emits the cancelable aui-catalog-add first; returns null when vetoed. Calls create for the element, then grid.addItem, then focuses the new item’s move handle (edit mode). Throws for an unknown template id. |
destroy(): void |
Remove the list, listeners, and the host class. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-layout-change |
DashboardGridLayoutChangeDetail { reason: 'move' | 'resize' | 'add' | 'remove' | 'undo' | 'redo' | 'set'; breakpoint: string; items: DashboardItem[]; layouts: Record<string, DashboardItem[]>; id: string | null } |
Yes | No | The layout changed: a drop or keyboard drop that changed positions or sizes (not every live step), addItem, removeItem or removal of the element, setLayout, undo, redo. Save detail.layouts. Dispatched on the grid host. |
aui-breakpoint-change |
DashboardGridBreakpointChangeDetail { breakpoint: string; columns: number } |
Yes | No | The host width crossed into another breakpoint. Dispatched on the grid host. |
aui-catalog-add |
DashboardCatalogAddDetail { template: DashboardTemplate; item: DashboardItem } |
Yes | Yes | DashboardCatalog: a template is about to be added (click, Enter, Space, drop, or add()). preventDefault() vetoes it. detail.item has the new id and the requested cell (x 0, y 0 when the first free spot will be used). Frameworks render the widget element (data-aui-item = detail.item.id) here. Dispatched on the catalog host. |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
children with data-aui-item |
Each host child element whose data-aui-item matches an item id is a grid item (the first child per id). The page or framework owns and renders them; the grid positions them, hides elements without an item in the active layout, reorders them within the host, and in edit mode prepends a move handle and appends a resize grip. Removing an element removes its item. Children without data-aui-item are left untouched. |
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-dashboard |
Grid host (display: grid). Custom properties set through the CSSOM: –aui-dashboard-columns, –aui-dashboard-row-height, –aui-dashboard-gap, –aui-dashboard-rows. |
aui-dashboard--editing |
Host in edit mode. |
aui-dashboard__item |
Every managed item element (position: relative). |
aui-dashboard__item--locked | --dragging |
Locked item (no resize grip) / the grabbed or dragged item (shadow, above others). |
aui-dashboard__handle |
Move handle button at the item’s top inline-end corner (aria-pressed while grabbed). |
aui-dashboard__resize |
Pointer-only resize grip at the bottom inline-end corner (aria-hidden). |
aui-dashboard__placeholder |
Drop target preview while moving, resizing, or dragging from the catalog (aria-hidden). |
aui-dashboard-catalog |
Catalog host. |
aui-dashboard-catalog__list | __entry | __template | __icon | __text | __label | __description |
Catalog list (ul), entry (li), template button, icon, and texts. |
aui-dashboard-catalog__template--dragging |
Template button while it is dragged. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
move handle |
grip |
catalog template (DashboardTemplate.icon) |
none |
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-strong | --aui-color-chart-axis | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-text | --aui-color-text-muted | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-size-sm | --aui-font-weight-medium | --aui-radius-md | --aui-radius-sm | --aui-shadow-lg | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-touch-target-min
TypeScript definitions
Section titled “TypeScript definitions”interface DashboardItem { id: string; x: number; y: number; w: number; h: number; minW?: number; minH?: number; maxW?: number; maxH?: number; locked?: boolean; label?: string }type DashboardCompaction = 'vertical' | 'none';interface DashboardBreakpoint { name: string; minWidth: number; columns: number }type DashboardLayoutReason = 'move' | 'resize' | 'add' | 'remove' | 'undo' | 'redo' | 'set';interface DashboardGridLayoutChangeDetail { reason: DashboardLayoutReason; breakpoint: string; items: DashboardItem[]; layouts: Record<string, DashboardItem[]>; id: string | null }interface DashboardGridBreakpointChangeDetail { breakpoint: string; columns: number }interface DashboardTemplate { id: string; label: string; description?: string; w: number; h: number; minW?: number; minH?: number; icon?: IconRef }interface DashboardCatalogAddDetail { template: DashboardTemplate; item: DashboardItem }Accessibility
Section titled “Accessibility”Pattern: Labelled region of items with a toggle-button move handle per item (keyboard drag-and-drop pattern, as Sortable List) and live-region announcements. No ARIA drag-and-drop attributes (aria-grabbed and aria-dropeffect are deprecated). The catalog is a labelled list of buttons.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Edit mode, nothing grabbed | Moves through the move handles and item content in DOM order, which follows the visual reading order (row, then column). |
| Enter / Space | Move handle focused, nothing grabbed | Grabs the item (aria-pressed=true); announces ‘{label} grabbed. Column {column}, row {row}, {width} by {height}. Arrow keys move, Shift and arrow keys resize, Enter drops, Escape cancels.’ A locked item announces ‘{label} is locked.’ and is not grabbed. |
| ArrowLeft / ArrowRight / ArrowUp / ArrowDown | Item grabbed | Moves the item one cell (Left/Right mirrored in RTL); other items are pushed down and float up live; announces ‘Column {column}, row {row}.’ Stops at the grid edges. |
| Shift+ArrowRight / Shift+ArrowLeft | Item grabbed | Widens / narrows the item by one column (in both directions), within minW/maxW and the column count; announces ‘{width} columns by {height} rows.’ |
| Shift+ArrowDown / Shift+ArrowUp | Item grabbed | Makes the item one row taller / shorter, within minH/maxH; announces the size. |
| Enter / Space | Grabbed item’s handle focused | Drops the item (compacting the layout); announces ‘{label} dropped at column {column}, row {row}.’; emits aui-layout-change (move or resize) when the layout changed. Focus stays on the handle. |
| Escape | Item grabbed | Cancels: the layout returns to where it was; announces ‘Move cancelled. {label} is back at column {column}, row {row}.’ The key event is consumed (preventDefault and stopPropagation). |
| Tab / Shift+Tab | Item grabbed | Drops the item at its current position (like Enter) and moves focus on. Leaving the handle any other way (blur) also drops. |
| Ctrl/Cmd+Z | Edit mode, focus inside the grid, nothing grabbed | Undo the last layout change (moves, resizes, setLayout); announces ‘Layout change undone.’ |
| Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y | Edit mode, focus inside the grid, nothing grabbed | Redo; announces ‘Layout change redone.’ |
| Enter / Space | Catalog template button focused | Adds the widget at the first free spot (after the cancelable aui-catalog-add); in edit mode focus moves to the new item’s move handle. |
| Tab / Shift+Tab | Catalog | Moves between template buttons (each is a tab stop). |
| Element | Role |
|---|---|
| grid host | region (role attribute), named by aria-label |
| item elements | unchanged (owned by the page; for example a Dashboard Widget region) |
| move handle <button> | button (native, toggle through aria-pressed) |
| resize grip, drop placeholder | none (aria-hidden=true) |
| catalog <ul> / <li> | list / listitem (native) |
| catalog template <button> | button (native) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-label |
grid host | label option |
aria-label and title |
move handle | Localized ‘Move {label}’: the item’s label, else its first heading (h1-h6 or role=heading) text, else its id |
aria-pressed |
move handle | true while the item is grabbed (keyboard) or dragged/resized (pointer), false otherwise |
aria-hidden |
resize grip, placeholder | true |
aria-label |
catalog list | Catalog label option |
aria-label |
catalog template button | Localized ‘Add {label}’ (the description is visible text only) |
live region |
shared live regions | Assertive: grabbed (with position, size, and instructions), moved, resized, dropped, cancelled. Polite: added, removed, undone, redone, locked. |
Focus stays on the grabbed item’s move handle during a keyboard move and after the drop; DOM reordering to reading order restores focus to the previously focused element (preventScroll). After a catalog add in edit mode, focus moves to the new item’s move handle. The handle shows a focus-ring-width outline in color-focus-ring.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- Dragging is never the only way to move or resize (WCAG 2.5.7): the move handle works with keyboard and with a single pointer (click the handle to grab, click again to drop); Shift+arrow keys resize.
- The DOM order follows the visual reading order after every change (reorderDom, default true), so focus order and screen-reader reading order match the layout.
- The grabbed state is conveyed by aria-pressed, a selected background, and a border (not color alone); forced-colors mode uses Highlight.
- Handles, grips, and catalog buttons are at least touch-target-min in size.
- Undo/redo shortcuts apply only in edit mode and not while an item is grabbed.
- 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 and pointer resizes are not announced.
- In edit mode the undo/redo shortcuts are handled on the whole host, so they also apply (and prevent native undo) while a text field inside a widget is focused.
Behavior
Section titled “Behavior”States
Section titled “States”view, edit mode, item grabbed (keyboard), item dragging or resizing (pointer), item locked, drop preview (catalog drag), per-breakpoint layout (stored or derived).
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| view | setEditable(true) / update({ editable: true }) |
edit mode: handles and grips added |
| edit mode | Enter/Space or click on a handle | grabbed (announced); placeholder shown |
| grabbed | arrow keys / Shift+arrows | item moved / resized live; others pushed down and floated up |
| grabbed | Enter/Space/click, Tab, or blur | dropped: layout compacted; aui-layout-change if changed; undo step |
| grabbed | Escape, breakpoint change, update() |
cancelled: layout restored |
| edit mode | pointer drag of the handle or grip | dragging/resizing; release drops, pointer cancel cancels |
| any | host width crosses a breakpoint | new breakpoint layout (stored or derived); aui-breakpoint-change |
| any | item element removed from the host | item removed from every layout; aui-layout-change (remove) |
Accessibility notes
Section titled “Accessibility notes”- Dragging is never required (WCAG 2.5.7): the handle works by keyboard and by single clicks (click to grab, click to drop); Shift+arrows resize, so the pointer-only grip has a keyboard equivalent.
- The DOM follows reading order after every change (keep
reorderDomon), so focus and screen-reader order match the layout; focus is restored after reordering. - The grabbed state is conveyed by
aria-pressed, background, and border, not color alone. - Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending; pointer moves are not announced.
Responsive behavior
Section titled “Responsive behavior”A ResizeObserver picks the breakpoint with the largest minWidth not above the host’s content width. Defaults: lg from 1024 px (12 columns), md from 640 px (6), sm from 0 (1). A breakpoint without a stored layout derives one from the widest layout: x and w scale proportionally (then overlaps are resolved and compacted), and a single column stacks items in reading order. The first edit at a derived breakpoint stores that layout; layouts then contains it. Row height and gap are fixed CSS px.
Right-to-left
Section titled “Right-to-left”Column 0 is at the inline start. With direction: 'rtl', ArrowLeft/ArrowRight moves are mirrored and pointer positions are measured from the right edge; the grip is mirrored. Shift+ArrowRight always widens and Shift+ArrowLeft narrows.
Theming
Section titled “Theming”Role tokens only. Handles: color-surface, color-border-strong; grabbed: color-selected + color-selected-text + border. Placeholder: color-selected with a dashed color-focus-ring border. Forced colors: Highlight for the grabbed handle and placeholder border, CanvasText grip lines. Item transitions are removed under prefers-reduced-motion and data-aui-animation="none".
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { dashboardGrid: { rowHeight: 96, gap: 24, compaction: 'none' } } });Global options: rowHeight, gap, compaction, plus inherited locale and direction. Changes apply to existing grids immediately.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic through update(); note that items, layouts, and breakpoints replace all stored layouts.
The move handle uses the grip icon. Catalog templates take an optional icon (IconRef).
Security
Section titled “Security”Placement is written with style.setProperty (never style attributes or HTML); CSP-safe. Labels and announcements are text. Items, layouts, and breakpoints are validated before use (DASHBOARD_GRID_OPTIONS), so layouts restored from storage can be passed back; malformed input throws rather than rendering.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. Save aui-layout-change detail.layouts (or grid.layouts) and pass it back as layouts. Item content stays owned by the page.
Performance
Section titled “Performance”Layout work is pure array computation over the items; rendering sets two placement properties per item and reorders only when the reading order changed. One ResizeObserver, one MutationObserver (child list), one host keydown listener, and per-item pointer trackers in edit mode. Budgets: DashboardGrid.create with 50 items < 40 ms in headless Chromium (e2e performance test); a keyboard move step with 50 items < 16 ms in headless Chromium (e2e performance test).
Supported data scale
Section titled “Supported data scale”50 items is the performance target. Columns 1..48 per breakpoint; rows are unbounded.
Edge cases
Section titled “Edge cases”- Vertical compaction: an item cannot rest below empty space, and a full-width item cannot pass a half-width neighbor (it floats back on drop). Use
compaction: 'none'for free placement. - Collisions push items down, never sideways; a locked item under the target pushes the moved item below it.
- Undo covers moves, resizes, and
setLayout; it restores geometry only and does not add back removed items or remove added ones. - A child whose id is not in the active layout is hidden; an item without an element occupies space but renders nothing.
- A host that is not laid out at create time (
clientWidth0) starts at the narrowest breakpoint until the observer reports its width. - In edit mode the undo/redo shortcuts are handled on the whole host, including text fields inside widgets.
- The catalog adds even when the grid is not editable; focus moves to the new handle only in edit mode.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
DASHBOARD_GRID_OPTIONS |
configuration | Empty label; invalid or duplicate item ids, positions, sizes, limits; invalid breakpoints (none with minWidth: 0, columns outside 1..48); invalid layouts, editable, reorderDom, historyLimit; addItem with a duplicate id. |
DASHBOARD_CATALOG_OPTIONS |
configuration | Empty label, missing grid, invalid or duplicate templates, add() with an unknown template id. |
A failed construction leaves the host untouched.
Known limitations
Section titled “Known limitations”- Vertical compaction rules above; no horizontal compaction.
- Pointer drags and resizes are not announced.
- No auto-scroll while dragging near the viewport edge.
update()withitems,layouts, orbreakpointsreplaces all stored layouts.- Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- Overlapping items or pixel positioning.
- Dragging items between grids.
- HTML5 drag and drop; dropping files.