Skip to content

Dashboard Grid

DashboardSince 0.4.0
import { DashboardGrid, DashboardCatalog, type DashboardItem } from '@absoluteui/components/dashboard-grid';
import '@absoluteui/components/dashboard-grid/style.css';

Using Angular? See Dashboard Grid for Angular.

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).

  • 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).
  • 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.

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();
});
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;
},
});
}
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.

<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>
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).

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.
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).
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.
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.
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.

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.
Slot Default icon
move handle grip
catalog template (DashboardTemplate.icon) none

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

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 }

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.

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)
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.

Criterion Level Note
1.3.1 Info and Relationships A
1.3.2 Meaningful Sequence A
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
2.5.7 Dragging Movements AA
2.5.8 Target Size (Minimum) AA
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • 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.

view, edit mode, item grabbed (keyboard), item dragging or resizing (pointer), item locked, drop preview (catalog drag), per-breakpoint layout (stored or derived).

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)
  • 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 reorderDom on), 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.

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.

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.

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".

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.

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).

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.

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.

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).

50 items is the performance target. Columns 1..48 per breakpoint; rows are unbounded.

  • 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 (clientWidth 0) 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.
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.

  • Vertical compaction rules above; no horizontal compaction.
  • Pointer drags and resizes are not announced.
  • No auto-scroll while dragging near the viewport edge.
  • update() with items, layouts, or breakpoints replaces all stored layouts.
  • Manual screen-reader verification is pending.
  • Overlapping items or pixel positioning.
  • Dragging items between grids.
  • HTML5 drag and drop; dropping files.