Skip to content

Data Grid

DataSince 0.9.0
import { DataGrid } from '@absoluteui/components/data-grid';
import '@absoluteui/components/data-grid/style.css';

Using Angular? See Data Grid for Angular.

A spreadsheet-like ARIA grid for large and wide data.

  • Users move cell by cell with the keyboard; the grid is one Tab stop.

  • Rows and columns are virtualized in one native scroll container, so a million rows and hundreds of columns stay fast.

  • Column groups, pinning to either edge, resizing, moving, multi-sort, paging, and a saved view state are built in.

  • Column filters (text, number, BigInt, date with relative and business-day ranges, boolean, set) through a filter panel, an optional floating filter row, a quick filter with an optional search box, and an application filter.

  • An advanced filter: an expression bar with type-ahead suggestions ([Name] contains "ann" and ([Age] >= 30 or [City] is any of ("Oslo", "Lima"))) and a builder dialog that edit the same tree of groups and conditions.

  • Row selection: single or multiple, a checkbox column with a tri-state Select all (filtered rows, every row, or the page), click to select with Ctrl and Shift, and selectable rules; kept by row id.

  • Cell ranges: drag, Shift+arrows, Ctrl+A, several ranges with Ctrl, whole columns from the header; Ctrl+C copies ranges, selected rows, or the focused cell as Excel-compatible text and an HTML table.

  • Writes without mutating rows: paste (tiled over a range), cut, Delete, fill down and right (Ctrl+D, Ctrl+R), and a fill handle that continues number and date series (business days with a business calendar); readOnlyEdits turns writes into requests the application applies.

  • Cell editing: text, large text, number, date, date-time (in the grid’s time zone), select, checkbox, and custom editors; validation that blocks (or reverts) with an announced message; bulk edit of a range with Ctrl+Enter; undo and redo of every write; full-row editing with cross-field validation; batch editing with pending values; searchable list editors (one value or several, values loaded per query) in a separate entry point.

  • Row grouping: several levels, a generated group column with counts, aggregates (sum, min, max, count, avg, first, last, or custom) in group rows, group and grand totals, keyboard expansion as a treegrid; a group panel; date hierarchies (year, quarter, month, week, day); aggregates shown as percentages; group checkboxes that select a group’s rows; one group column or one per level.

Tree data, master/detail, pivot mode, and server row models are added in the next steps and phases of the Data Grid plan.

  • Large or wide data sets users explore cell by cell with the keyboard.
  • Many columns that need pinning, groups, resizing, and reordering.
  • Hundreds of thousands to millions of rows in the browser.
  • Read-mostly tables where every control is reached with Tab: use Data Table (plain table semantics).
  • Page layout of non-tabular content: use Layout.

Pinned and grouped columns over a million rows

Section titled “Pinned and grouped columns over a million rows”
import { DataGrid, type DataGridSortChangeDetail } from '@absoluteui/components/data-grid';
interface Trade {
id: string;
symbol: string;
price: number;
qty: number;
}
const rows: Trade[] = Array.from({ length: 1_000_000 }, (_, i) => ({ id: String(i), symbol: `S${String(i % 500)}`, price: (i % 997) / 10, qty: i % 300 }));
const host = document.createElement('div');
host.style.setProperty('block-size', '480px');
document.body.append(host);
const grid = DataGrid.create<Trade>(host, {
label: 'Trades',
rows,
rowId: (r) => r.id,
columns: [
{ id: 'symbol', header: 'Symbol', field: 'symbol', pinned: 'start', width: 120 },
{ id: 'deal', header: 'Deal', children: [
{ id: 'price', header: 'Price', field: 'price', type: 'currency', currency: 'EUR' },
{ id: 'qty', header: 'Quantity', field: 'qty', type: 'number', flex: 1 },
] },
],
});
grid.host.addEventListener('aui-sort-change', (event) => {
console.log((event as CustomEvent<DataGridSortChangeDetail>).detail.sort);
});
import { DataGrid } from '@absoluteui/components/data-grid';
const host = document.createElement('div');
document.body.append(host);
const grid = DataGrid.create(host, {
label: 'People',
rows: [{ id: 'a', name: 'Ada' }],
rowId: (r) => r.id,
columns: [{ id: 'name', header: 'Name', field: 'name' }],
pageSize: 50,
});
grid.host.addEventListener('aui-columns-change', () => {
localStorage.setItem('people-view', JSON.stringify(grid.getState()));
});
const saved = localStorage.getItem('people-view');
if (saved) grid.setState(JSON.parse(saved));
import { DataGrid, type DataGridCellActivateDetail } from '@absoluteui/components/data-grid';
import '@absoluteui/components/data-grid/style.css';
interface Order {
id: string;
total: number;
}
const host = document.querySelector<HTMLElement>('#orders');
if (host) {
DataGrid.create<Order>(host, { label: 'Orders', rows: [], rowId: (r) => r.id, columns: [{ id: 'total', header: 'Total', field: 'total', type: 'currency', currency: 'EUR' }] });
host.addEventListener('aui-cell-activate', (e) => {
console.log((e as CustomEvent<DataGridCellActivateDetail<Order>>).detail.rowId);
});
}
import { DataGrid } from '@absoluteui/components/data-grid';
const host = document.createElement('div');
host.style.setProperty('block-size', '400px');
document.body.append(host);
DataGrid.create(host, {
label: 'People',
rows: [{ id: '1', name: 'Ada', city: 'London' }],
rowId: (r) => r.id,
columns: [
{ id: 'name', header: 'Name', field: 'name', pinned: 'start' },
{ id: 'city', header: 'City', field: 'city', flex: 1 },
],
});

Filtering: a filter kind per column, the floating filter row, and the search box. Restore filters from a URL with setState (validated).

import { DataGrid } from '@absoluteui/components/data-grid';
const host = document.createElement('div');
host.style.setProperty('block-size', '400px');
document.body.append(host);
DataGrid.create(host, {
label: 'People',
rows: [{ id: '1', name: 'Ada', age: 36, city: 'London' }],
rowId: (r) => r.id,
floatingFilters: true,
searchBox: true,
filters: { age: { kind: 'number', conditions: [{ operator: 'greaterThanOrEqual', value: 30 }] } },
columns: [
{ id: 'name', header: 'Name', field: 'name', filter: 'text' },
{ id: 'age', header: 'Age', field: 'age', type: 'number', filter: 'number' },
{ id: 'city', header: 'City', field: 'city', filter: 'set' },
],
});

List editors (a separate entry point): a searchable list for one value and chips for several.

import { DataGrid } from '@absoluteui/components/data-grid';
import { comboboxEditor, multiSelectEditor } from '@absoluteui/components/data-grid/editors';
interface Task {
id: string;
owner: string;
tags: string[];
}
const host = document.createElement('div');
host.style.setProperty('block-size', '400px');
document.body.append(host);
DataGrid.create<Task>(host, {
label: 'Tasks',
rows: [{ id: '1', owner: 'Ada', tags: ['ui'] }],
rowId: (r) => r.id,
columns: [
{ id: 'owner', header: 'Owner', field: 'owner', editable: true, editor: comboboxEditor({ values: [{ value: 'Ada', label: 'Ada' }, { value: 'Grace', label: 'Grace' }] }) },
{ id: 'tags', header: 'Tags', field: 'tags', editable: true, editor: multiSelectEditor({ values: [{ value: 'ui', label: 'UI' }, { value: 'api', label: 'API' }] }), format: (v) => (Array.isArray(v) ? v.join(', ') : '') },
],
});

Advanced filter: an expression bar and builder over the same model, here set from code.

import { DataGrid } from '@absoluteui/components/data-grid';
const host = document.createElement('div');
host.style.setProperty('block-size', '400px');
document.body.append(host);
DataGrid.create(host, {
label: 'People',
rows: [{ id: '1', name: 'Ada', age: 36, city: 'London' }],
rowId: (r) => r.id,
advancedFilter: true,
advancedFilterModel: {
join: 'or',
items: [
{ column: 'age', filter: { kind: 'number', conditions: [{ operator: 'greaterThanOrEqual', value: 30 }] } },
{ column: 'city', filter: { kind: 'set', values: ['Oslo', 'Lima'] } },
],
},
columns: [
{ id: 'name', header: 'Name', field: 'name', filter: 'text' },
{ id: 'age', header: 'Age', field: 'age', type: 'number', filter: 'number' },
{ id: 'city', header: 'City', field: 'city', filter: 'set' },
],
});
<div class="aui-data-grid"> (consumer host, needs a height)
<div class="aui-data-grid__toolbar">
<div role="search"> <label> Search <input type="search"> (searchBox: true)
<div class="aui-data-grid__advanced" role="group"> (advancedFilter: true)
<label>Filter</label> <input role="combobox"> <div role="listbox"> Apply Builder Clear error
<div class="aui-data-grid__viewport"> one scroll container
<div role="grid" aria-label aria-rowcount aria-colcount>
<div role="rowgroup" class="aui-data-grid__head"> sticky header rows
<div role="row"> <div role="columnheader"> ... (groups: aria-colspan; leaves may span rows)
<button class="aui-data-grid__filter-button"> filterable leaves (tabindex -1)
<div role="row" class="aui-data-grid__floating-row"> <div role="gridcell"> control (floatingFilters: true)
<div role="rowgroup" class="aui-data-grid__body"> virtualized rows
<div role="row" aria-rowindex> <div role="gridcell" aria-colindex> ...
<div class="aui-data-grid__pager"> Pagination (when pageSize is set)
<div class="aui-data-grid__overlay"> loading
<div class="aui-data-grid__filter-panel" role="dialog"> open filter panel (top layer)
</div>
<dialog class="aui-dialog aui-data-grid__builder-dialog"> builder (a modal Dialog in the document body)
DataGrid.create<Row>(host: HTMLElement, options: DataGridOptions<Row>): DataGrid<Row>

Global configuration key: dataGrid (see Configuration).

Option Type Default Description
label string Accessible name of the grid. Required.
description string Accessible description of the grid (aria-describedby).
columns readonly (DataGridColumn<Row> | DataGridColumnGroup<Row>)[] Columns and column groups (nested). Column: id, header, accessor or field, type, currency, format, render, align, width, minWidth, maxWidth, flex, resizable, movable, pinned, lockPinned, lockPosition, hidden, sortable, compare, headerTooltip, wrapHeader, wrapText, rowDrag (this column holds the row drag handle; default the first data column), tooltip (true, a text, or (value, row) => text; with the tooltips accessory), spanRows (true or (a, b) => boolean: merge equal adjacent values), colSpan (row => columns covered), cellClass, cellClassRules, flashOnChange, copyValue, exportable, formula (a calculated column with the formulas option: [Column] is the same row, A1 references and functions as in spreadsheets; read-only), formulas (cell formulas: values that are text starting with = are evaluated; editing shows the formula; fill, Ctrl+D, and paste of copied cells move relative A1 references), excel (Excel export: style, cellStyle(row), numberFormat, formula(row, ref), link(row), image(row), width) (false leaves the column out of CSV and Excel exports of visible or all columns), aggregate, showAs, groupable, groupHierarchy, editable, setValue, parseValue, editor, required, validate, filter, filterValue, filterCaseSensitive, filterAccentSensitive, filterValues, businessCalendar, searchable, visibleWhen. Group: id, header, children, collapsible, collapsed, headerTooltip. Required.
rows readonly Row[] Row data (never mutated). Required.
rowId (row: Row) => string Stable, unique, non-empty id per row. Required.
rowHeight number | ((row: Row) => number) Row height in pixels (16 to 1000), or a function of the row. Default by density: 32, 40, or 48.
autoRowHeight boolean false Measure rendered rows and let them grow to their content (with wrapText columns); the scroll position stays anchored.
layout 'fill' | 'auto' "fill" fill: fills the host (which needs a height). auto: grows with its rows up to the host max-block-size.
sort DataGridSort | readonly DataGridSort[] | null Current sort by priority: { column, direction: “ascending” | “descending” }.
pageSize number | null null Rows per page (1 to 1,000,000); null shows every row in one virtualized grid.
pageSizes readonly number[] Offer a “Rows per page” choice with these sizes.
page number 1 Current page, 1-based (clamped).
filters DataGridFilters Active column filters by column id (JSON models: text, number, bigint, date with relative and business ranges, boolean, set). Validated; DATA_GRID_FILTER on malformed models.
quickFilter string Rows must contain every word in some searchable visible column (case- and accent-insensitive).
externalFilter (row: Row) => boolean Application filter applied with the others; call refreshFilters() when its result may change.
floatingFilters boolean false A header row of compact filter controls under the column headers: text contains, number syntax (5, >5, <=10, !=0, 5..10), date, boolean choice, set summary; complex filters show a read-only summary.
searchBox boolean false A search box above the grid (role search) that sets the quick filter as the user types.
advancedFilter boolean false An expression bar with type-ahead suggestions and a builder dialog replaces the column filter UI; column filters stop applying while it is on.
advancedFilterModel DataGridAdvancedFilter | null The advanced filter: { join, items } of conditions { column, filter } and nested groups. Validated (DATA_GRID_FILTER); conditions on unknown columns are dropped.
rowSelection DataGridRowSelection<Row> | null Row selection: { mode: ‘single’ | ‘multiple’, checkboxes (default true; a checkbox column pinned first), headerCheckbox (default true), selectAll: ‘filtered’ (default) | ‘all’ | ‘page’, clickToSelect (default false), selectable(row) }.
selected readonly string[] Selected row ids (unknown and unselectable ids are dropped; kept by id across sort, filter, paging, and updates).
cellSelection boolean | DataGridCellSelection Cell ranges: drag, Ctrl+drag adds, Shift+click and Shift+arrows extend, Ctrl+A selects all; { multiple (default true), columnSelection (header Enter or click selects the column; sorting moves to Alt), fillHandle (drag the corner to fill a series; Alt repeats or counts; double-click fills down) }.
copyHeaders boolean false Copy column headers above copied cells.
readOnlyEdits boolean false Writes (paste, cut, Delete, fill, edits) are only requested: aui-cells-change reports them with applied: false and the application applies them.
clickToEdit boolean false A single click on an editable cell starts editing (otherwise Enter, F2, typing, Backspace, or a double-click).
enterMovesDown boolean false Enter commits an edit and moves to the cell below.
invalidEdit 'block' | 'revert' "block" An invalid edit keeps the editor open with an announced message (block), or is discarded (revert). Focus leaving the editor discards an invalid value.
undoLimit number 20 Undo steps kept for edits, paste, cut, Delete, fill, and bulk edit (0 turns undo off); cleared when the application replaces or updates rows.
editMode 'cell' | 'row' "cell" row: editing opens every editable cell of the row; Tab moves within it; Enter commits the row as one write and one undo step.
validateRow (values, row) => Record<string, string> | string | null Row mode: cross-field validation; a message (or messages by column) blocks the commit.
groupBy readonly string[] Group the rows by these columns (in level order). The grid becomes a treegrid with a generated group column (key, count, expansion); grouped columns hide (hideGroupedColumns).
groupDefaultExpanded number 0 Levels expanded by default (0: all collapsed; -1: all expanded).
groupTotals 'bottom' | null A total row after each expanded group.
grandTotal 'top' | 'bottom' | 'pinnedTop' | 'pinnedBottom' | null A grand total row at the top or bottom of the rows, or pinned above or below them (pinnedTop, pinnedBottom); flat grids too.
groupDisplay 'column' | 'columns' | 'rows' "column" One generated group column indented by level, one per level (named after the grouped column; date parts in parentheses), or rows: group rows drawn as one cell across the row (no group column; not for tree data or pivot mode).
stickyGroupRows boolean true While grouped, the group rows of the topmost row stay under the header as their groups scroll (copies hidden from assistive technology; their toggles work).
fullWidthRow { isFullWidth: (row: Row) => boolean; render: (row: Row, context: { doc, rowId }) => Node } | null Rows drawn as one cell across every column (it stays in view while the rows scroll sideways); never edited, filled, or part of ranges.
groupColumn { header?: string; width?: number } Header text and width of the generated group column.
hideGroupedColumns boolean true Hide grouped columns while grouped.
expanded Readonly<Record<string, boolean>> Group rows expanded or collapsed explicitly, by group id.
groupPanel boolean false A panel above the grid lists the row groups: move earlier, move later, remove, add a groupable column; dropping a groupable header on it groups by that column; pills can be dragged to reorder, or out of the panel to remove (the buttons are the keyboard path).
aggregateFilteredOnly boolean true Aggregates use only the rows that pass the filters; false aggregates every row of each group.
accessories readonly DataGridAccessory<Row>[] Accessories from @absoluteui/components/data-grid/accessories: columnMenu(), contextMenu(), columnChooser() (built on Context Menu and Dialog, outside the core bundle). The grid must be in the document when they attach.
headerNames Readonly<Record<string, string>> Header names set by the user, by column or column group id (part of the columns state as headers).
pinnedTopRows readonly Row[] Rows always shown above the body in a section named “Pinned to top” (never sorted, filtered, grouped, selected, or edited; at most 1000).
pinnedBottomRows readonly Row[] Rows always shown below the body in a section named “Pinned to bottom”.
pinned Readonly<Record<string, "top" | "bottom">> Rows pinned to the top or bottom by id (they also stay in the body; filters hide the pinned copy).
formulas DataGridFormulaEngine The formula engine for calculated columns: formulaEngine({ functions?, calendar? }) from @absoluteui/components/data-grid/formulas (our own parser and evaluator; no eval). Results recalculate when rows change; errors show their code (#DIV/0!, #REF!, …) with the meaning read out.
fileDrop { onFiles: (files) => void; accept?: string[]; label?: string } Files dropped on the grid: an overlay (aui-data-grid__file-drop, default text “Drop files to load”) shows while files are over it; files matching accept (extensions such as .csv, MIME types such as text/csv or text/*; default all) go to onFiles, others are announced. Pointer only: offer a file input too. With parseCsv from @absoluteui/helpers/csv, coerceCsvValues, and inferColumns an app can load a dropped CSV with generated columns.
rowDragging boolean | { managed?: boolean; multiRow?: boolean; canDrag?: (row) => boolean; nativeData?: (rows) => Record<string, string> } false Row dragging: a drag handle in the rowDrag column (pointer: a drop line, or a target row in parent-id tree data), and Ctrl+Shift+ArrowUp / ArrowDown on a handle cell (in trees Ctrl+Shift+ArrowRight / ArrowLeft indent and outdent); announced. managed: the grid moves the rows (one undo step) while not sorted, filtered, grouped, pivoted, or server-side; multiRow: dragging a selected row drags every selected row; canDrag limits the rows. nativeData: the handle uses native drag and drop and the drag carries that data (MIME type to text) to other apps; drops on the rows and on zones work as with the pointer. Drop zones: addRowDropZone, rowDropZone.
rowNumbers boolean | { width?: number; resizable?: boolean } false A leading column numbering the rows by display position (width 32 to 200, default 56); with cell ranges a click on a number selects the row’s cells; resizable adds a handle (drag, or Alt+ArrowUp / Alt+ArrowDown on the number) that sets the row’s height.
datasource DataGridDatasource<Row> | null Server row model: { getRows(request, signal) => Promise<{ rows, rowCount? }> } (request: start, end, sort, filters, quickFilter, advancedFilter, groupBy, groupKeys); with groupBy the top level loads group rows and each expanded group loads its own rows; rows load in blocks as they scroll into view, and sort, filters, the quick filter, and the advanced filter run on the server (rows is ignored; pass []). An unknown rowCount grows block by block until a short block.
blockSize number 100 Rows per datasource request (1 to 10000).
maxBlocks number | null null Loaded blocks kept; the least recently used are released (null keeps all).
blockLoadDelay number 50 Milliseconds to wait before loading while scrolling (0 to 5000).
maxConcurrentLoads number 2 Datasource requests at a time (1 to 20).
initialRowCount number 0 Rows assumed before the first answer (the scrollbar size).
getChildCount (row: Row) => number | null Server groups: the number of rows below a group row (shown with its key); null when unknown.
purgeClosedGroups boolean false Server groups: collapsing a group releases its loaded rows (they load again when it expands).
pivot DataGridPivot | null Pivot mode: { rows, columns, values: [{ column, aggregate: sum | count | avg | min | max | distinctCount }] }; rows group by the row fields, a column group per column key holds a result column per value, with total columns; the data columns hide.
pivotPanel boolean false A panel above the grid edits the pivot: rows, columns, and values zones with pills (move, remove; an aggregate per value) and pickers; pills can be dragged within and between zones (a value keeps its aggregate, a new value gets sum or count) or out of the panel to remove.
pivotMaxColumns number 1000 Most generated pivot result columns (1 to 100000); more are left out, announced, and reported by getPivotTruncation().
detail DataGridDetail<Row> | null Master/detail: { render(row, { doc, rowId }) => Node, height (px, default 300, or auto), isMaster(row), keep (collapsed details kept, default 10), expandedByDefault, destroy(node, row) }; a generated toggle column expands a detail row under each master row.
treeData DataGridTreeData<Row> | null Tree data: exactly one of serverGroup with serverKey (with a datasource: parents load their children from the server when expanded), getPath (row => string[]; missing levels become filler rows), parentIdField (self-referential; missing parents and cycles become root rows), or childrenField (nested rows); label names the column that labels rows in the group column. groupBy is ignored.
treeFilter "ancestors" | "subtree" "ancestors" Filtering keeps the ancestors of matching rows; subtree also keeps the rows below a matching row.
flashChanges boolean false Highlight cells whose displayed text changes when rows are updated (setRows, transactions); columns can opt in with flashOnChange.
flashDuration number 800 Milliseconds a changed cell stays highlighted (100 to 10000).
rowClassRules Readonly<Record<string, (row: Row) => boolean>> Classes added to a row while their predicate is true (re-evaluated on every render).
loading boolean false Busy state: aria-busy and an announced loading overlay; rows stay in place.
emptyMessage string Text of the single grid cell shown when there are no rows. Default “No rows to show”.
multiSort boolean true Shift+click and Shift+Enter add a column to the sort. Globally configurable.
sortCycle 'tri-state' | 'bi-state' "tri-state" tri-state: ascending, descending, unsorted. bi-state: ascending, descending. Globally configurable.
overscan number 8 Rows rendered beyond each edge of the viewport (0 to 200). Globally configurable.
transactionDelay number 50 Milliseconds between applying queued applyTransactionAsync batches (0 to 10000). Globally configurable.
animateRows boolean true Rows glide to their new position after a sort or update (off with reduced motion or animation none). Globally configurable.
filterDelay number 250 Milliseconds after typing stops before a typed filter value applies (0 to 10000); Enter applies at once. Globally configurable.
maxFilterConditions number 2 Conditions offered per text, number, and date filter in the filter panel (1 to 10). Globally configurable.
locale string "en-US" Formatting, collation, and messages locale. Globally configurable. Inherited from common configuration.
timezone string "UTC" IANA time zone for datetime columns. Globally configurable. Inherited from common configuration.
density 'compact' | 'comfortable' | 'spacious' "comfortable" Row and header heights and cell padding. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" Layout direction (pinned sides, arrow keys, resizing mirror). Globally configurable. Inherited from common configuration.
icons Partial<DataGridIcons> Icon slot overrides (sortAscending, sortDescending, sortNone, expand, collapse, loading, filter, drag).
Property Type Description
host HTMLElement Consumer-provided host (children are owned by the grid).
element HTMLDivElement The element with role=“grid”.
rowCount number Rows displayed (the current page when paging).
sort DataGridSort[] Current sort by priority (a copy).
focusedCell DataGridCellPosition | null The cell or header holding the tab stop: { rowId, column } or { rowId: null, column, level }.
filteredRowCount number Rows that pass the filters (before paging).
editingCell { rowId, column } | null The cell being edited.
canUndo boolean An undo step is available.
canRedo boolean A redo step is available.
isBatchEditing boolean A batch is collecting pending values.
Method Description
update(options: Partial<DataGridOptions<Row>>): void Merge and apply options.
setRows(rows: readonly Row[]): void Replace the rows; sort, focus, and scroll position are kept.
applyTransaction(tx: DataGridTransaction<Row>): DataGridTransactionResult Add, update (by id), and remove (by id) rows; sort is re-applied and rows keep their identity. Emits aui-rows-change; unknown ids are reported in missing.
applyTransactionAsync(tx: DataGridTransaction<Row>): void Queue a transaction; queued ones are applied together every transactionDelay ms.
flushTransactions(): void Apply queued transactions now.
getFilters(): DataGridFilters Active column filters (copies).
setFilter(column: string, filter: DataGridFilter | null): void Set or clear one column filter (validated; emits nothing).
setQuickFilter(text: string): void Set the quick filter text (emits nothing).
refreshFilters(): void Re-run the filters (for example after the external filter changed).
getAdvancedFilter(): DataGridAdvancedFilter | null The advanced filter model (a copy).
setAdvancedFilter(filter: DataGridAdvancedFilter | null): void Set or clear the advanced filter model (validated; emits nothing).
getSelected(): string[] Selected row ids, in source order.
getSelectedRows(): Row[] Selected rows, in source order.
setSelected(ids: readonly string[]): void Replace the selection (validated; emits nothing).
selectAll(scope?: 'filtered' | 'all' | 'page'): void Select every selectable row in the scope (default: rowSelection.selectAll; emits nothing).
clearSelection(): void Clear the selection (emits nothing).
getCellRanges(): DataGridCellRange[] Cell ranges (copies), the active one last.
addCellRange(range: DataGridCellRange): void Add a range of displayed rows and visible columns (validated; emits nothing).
clearCellRanges(): void Clear the cell ranges (emits nothing).
copySelection(): Promise<string> Copy the ranges, else the selected rows, else the focused cell, with the asynchronous Clipboard API; returns the tab-separated text.
startEditing(rowId: string, column: string): boolean Start editing a displayed, editable cell.
stopEditing(cancel?: boolean): void Commit the open edit (an invalid value is discarded) or cancel it.
undo(): boolean Undo the last applied write or managed row move.
redo(): boolean Redo the last undone write.
startBatch(): void Collect writes as pending values (shown and spoken as edited; sorting and filtering keep committed data).
commitBatch(): void Apply the pending values as one transaction and one undo step (source batch).
cancelBatch(): void Discard the pending values.
expandAll(): void Expand every group row (emits nothing).
collapseAll(): void Collapse every group row (emits nothing).
setExpanded(groupId: string, expanded: boolean): boolean Expand or collapse one group row (emits nothing).
getGroups(): DataGridGroup[] The current groups: ids, columns, keys, counts, aggregates, and children.
moveRow(id: string, parentId: string | null, index: number): boolean Tree data with parentIdField: move a row under another parent (null: a root) at a position among its new siblings; the row is a shallow copy (never mutated); false for unknown ids, moves into its own subtree, and other tree modes. Emits aui-rows-change.
alignWith(other: DataGrid<Other>): () => void Keep another grid’s columns (order, widths, pinning, visibility, collapsed groups) and horizontal scroll in step with this one, both ways (for example a totals grid under the main grid); the other grid takes this grid’s columns now. Returns a function ending it; destroying either grid ends it.
addRowDropZone(zone: DataGridRowDropZone<Row>): () => void Let rows dragged from this grid drop on an element outside it: { element, label, onDrop, onDragEnter?, onDragMove?, onDragLeave? }, each called with { rowIds, rows, x, y, grid }. The element gets aui-data-grid-drop-zone–active while rows are over it; the cell menu’s Send to submenu lists zones by label (the keyboard path, x and y null). Returns a function removing the zone.
rowDropZone(options?: { label?: string; move?: boolean }): DataGridRowDropZone<Row> A zone for another grid’s addRowDropZone adding the dropped rows to this grid (at the drop line when its rows are in source order, else at the end; ids already present are skipped); move removes the added rows from the source grid.
setColumnHeader(id: string, header: string | null): boolean Rename a column or column group (null restores its defined header); false for unknown ids. Emits aui-columns-change (rename).
resetColumns(): void Undo the user’s column changes (order, widths, pinning, visibility, collapsed groups, names). Emits aui-columns-change (reset).
pinRow(rowId: string, side: "top" | "bottom" | null): boolean Pin a row by id to the top or bottom, or unpin it; false for unknown ids (emits nothing).
getPinnedRows(): { top: string[]; bottom: string[] } Ids of the rows pinned by id, by section, in pin order.
setRowHeight(rowId: string, height: number | null): void Set a row’s height in px (16 to 2000), or null for its default height (emits nothing).
applyServerTransaction(tx: DataGridServerTransaction<Row>): DataGridTransactionResult Server row model: apply changes the server already holds to the loaded rows (add at addIndex, update, remove by id; later rows shift; rowCount corrects the total); rows that are not loaded are reported missing. Emits aui-rows-change.
getServerSelection(): { selectAll: boolean; toggled: string[] } Server row model: the selection state; with selectAll, toggled lists the rows not selected.
setServerSelection(state: { selectAll: boolean; toggled: string[] }): void Server row model: set the selection state (multiple selection only; emits nothing).
refresh(options?: { purge?: boolean; route?: readonly unknown[] }): void Server row model: reload the loaded blocks; purge drops them first (loading rows show).
retryLoads(): void Server row model: load failed blocks again (never done automatically).
setRowCount(count: number, known?: boolean): void Server row model: set the row count; known false keeps growing it as blocks arrive.
isRowCountKnown(): boolean Server row model: whether the total row count is known (always true for client rows).
getPivot(): DataGridPivot | null Pivot mode: the current pivot, or null.
setPivot(pivot: DataGridPivot | null): void Pivot mode: set the pivot; null leaves pivot mode (emits nothing).
getPivotTruncation(): { shown: number; total: number } | null Pivot mode: result columns shown and in total when pivotMaxColumns left some out, else null.
setDetailExpanded(rowId: string, expanded: boolean): boolean Master/detail: expand or collapse a master row’s details; false for rows without details (emits nothing).
isDetailExpanded(rowId: string): boolean Master/detail: whether a master row’s details are expanded.
getTreeOrphans(): string[] Tree data: ids of rows whose parent is missing or would form a cycle (shown as root rows).
toCsv(options?: DataGridCsvOptions<Row>): string RFC 4180 CSV with formula-injection protection (text starting with =, +, -, @, a tab, or a carriage return is prefixed with a quote; numbers the grid formats itself are trusted). Options: columns (visible, all, or ids), rows (filtered: across pages with group rows and totals, every group expanded; page; selected and all, ignoring filters; range: the active cell range), groups (rows or none), header, columnGroups (group rows above the header), pinned, values (formatted or raw), indent (per level for group and tree labels), delimiter, lineEnding, bom, escapeFormulas, and value(cell) for custom values. With a datasource only loaded rows exist.
toCsvBlob(options?: DataGridCsvOptions<Row>): Blob toCsv() as a text/csv Blob; downloadCsv from the Data Table entry saves it.
cellAddress(rowId: string, column: string): string | null A cell’s stable A1 address for formulas (column letters by column definition order, row numbers by source row order), or null without formula columns.
cellFormula(rowId: string, column: string): string | null A cell’s formula: the calculated column’s formula or the cell formula; null for plain values.
getExportData(options?: DataGridExportOptions): DataGridExportData<Row> The rows and columns of an export for exporters such as toXlsx in @absoluteui/components/data-grid/export: the toCsv choices (columns, rows, groups, pinned), raw values (aggregates as numbers), display text (text: “needed” formats only where required), group levels, column group header runs, and the expansion state. No side effects.
print(options?: DataGridPrintOptions): void Print the rows as a static table shown only in print media (the page itself is hidden while printing): the toCsv row and column choices (columns, rows, groups, pinned), a title (default: the grid label) as the caption, column group headers repeated on every page, group and total rows, the grid direction, and print colors. The table is removed after afterprint. More than 20,000 rows throws DATA_GRID_PRINT (page the grid or export CSV).
getState(): DataGridState Serializable view state: sort, columns (order, widths, pinned, hidden, collapsed groups), page, page size; includes details (master rows expanded or collapsed), pinnedRows (pinned by id), and pivot.
setState(state: unknown): void Restore a view state; untrusted input is validated (DATA_GRID_STATE), unknown ids are dropped; emits nothing.
getColumnsState(): DataGridColumnsState The columns section of the state.
moveColumn(id: string, target: string, position?: 'before' | 'after'): boolean Move a column or group next to a sibling with the same parent group.
setColumnPinned(id: string, pinned: DataGridPinned | null): boolean Pin to the start or end edge, or unpin.
setColumnHidden(id: string, hidden: boolean): boolean Hide or show a column (one always stays visible).
autoSizeColumns(ids?: readonly string[]): void Size columns to their header and rendered cells.
fitColumns(): void Scale the visible columns to the viewport width.
focusCell(rowId: string, column: string): boolean Move focus to a cell, scrolling it into view.
scrollToRow(rowId: string): boolean Scroll a row into view.
scrollToColumn(column: string): boolean Scroll a column into view.
destroy(): void Remove the grid, listeners, and released nodes.
Event Detail Bubbles Cancelable Description
aui-sort-change { sort: DataGridSort[] } Yes No The user changed the sort.
aui-rows-change { added, updated, removed, missing } Yes No A transaction (sync or batched) changed the rows.
aui-filter-change { filters, quickFilter, advancedFilter } Yes No The user changed a column filter, the quick filter, or the advanced filter; the row count is announced.
aui-columns-change { reason, columns: DataGridColumnsState } Yes No The user (or a method) resized, moved, pinned, hid, auto-sized, or fitted columns, or toggled a column group.
aui-page-change { page, pageSize } Yes No The user changed the page or the page size.
aui-cell-focus { rowId: string | null, column } Yes No Focus moved to another cell or header.
aui-cell-activate { rowId, column, row } Yes No Enter on a cell without widgets, or a double click.
aui-selection-change { selected, added, removed, reason } Yes No The user changed the row selection (‘user’, ‘select-all’), or selected rows were removed (‘rows’); the count is announced for user changes.
aui-range-change { ranges, finished } Yes No The user changed the cell ranges (finished is false while a drag extends the range).
aui-cells-change { changes: [{ rowId, column, oldValue, newValue }], source, applied, rejected } Yes No Cells were written by an edit, bulk edit, paste, cut, Delete, fill, undo, or redo (one transaction; with readOnlyEdits only requested, applied: false); rejected lists pasted text a column could not convert.
aui-row-group-toggle { groupId, expanded } Yes No The user expanded or collapsed a group row (announced with its row count).
aui-group-change { groupBy } Yes No The user changed the row groups in the group panel (announced).
aui-detail-toggle { rowId, expanded } Yes No The user expanded or collapsed a master row’s details (announced).
aui-row-pin { rowId, pinned } Yes No The user pinned or unpinned a row (announced).
aui-row-drag-start { rowIds } Yes No The user started dragging rows with a drag handle.
aui-row-drop { rowIds, targetId, position, source } Yes Yes Rows were dropped before, after, or (tree data) inside a target row, by pointer or keyboard; cancel to keep the order. Managed grids then move the rows.
aui-row-resize { rowId, height } Yes No The user resized a row with its row number handle (announced).
aui-pivot-change { pivot } Yes No The user changed the pivot in the pivot panel (announced).

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-data-grid Host; aui-data-grid–compact / –spacious / –auto modifiers.
aui-data-grid__viewport The single scroll container (both axes).
aui-data-grid__head | __header-row | __header-cell | __group-header Sticky header rows, leaf and group header cells.
aui-data-grid__body | __row | __cell Virtualized rows and cells; __cell–end / –center alignment, –wrap, –pinned-start / –pinned-end.
aui-data-grid__resize | __drop-indicator Column resize handle and column drop position.
aui-data-grid__overlay | __empty | __pager Loading overlay, empty-state cell, pagination host.
aui-data-grid__filter-button | __header-cell--filtered Filter button in filterable headers (–active while filtered) and the filtered header state.
aui-data-grid__filter-panel The filter panel (non-modal dialog in the top layer): __filter-title, __filter-form, __filter-condition, __filter-join, __filter-set, __filter-values, __filter-clear.
aui-data-grid__floating-row | __floating-cell | __floating-control Floating filter row, its cells, and their compact controls.
aui-data-grid__toolbar | __search | __search-input Toolbar (role search) with the quick filter search box.
aui-data-grid__advanced | __advanced-input | __advanced-list | __advanced-option | __advanced-error Advanced filter expression bar: combobox field, suggestion listbox, and error message.
aui-data-grid__builder-dialog | __builder-group | __builder-condition | __builder-tools Builder dialog (a Dialog): groups as fieldsets, condition rows, item buttons.
aui-data-grid__row--selected | __select-input | __select-all | __select-header Selected rows, row checkboxes, the Select all checkbox, and the selection column header.
aui-data-grid__cell--range | --range-top | --range-bottom | --range-start | --range-end Cells inside cell ranges and the range edges.
aui-data-grid__fill-handle | __cell--fill The fill handle in the range corner and the cells a fill drag would cover.
aui-data-grid__cell--editing | __editor | __editor--popup | __editor-control | __edit-error The edited cell, the editor (inline or a popup over the cell), its control, and the validation message.
aui-data-grid__row--editing | __cell--pending The row edited in row mode, and cells with pending batch values.
aui-data-grid__row--group | __row--total | __group-cell | __group-toggle Group and total rows, the group column cell (indented by level), and the expansion toggle.
aui-data-grid__group-panel | __group-panel--drop | __group-pill | __group-pill-button | __group-add The group panel, its drop state, pills and their buttons, and the add picker.
aui-data-grid__select-group A group row checkbox (tri-state over its rows).
Slot Default icon
sortAscending arrow-up
sortDescending arrow-down
sortNone arrow-up-down
expand chevron-right
collapse chevron-down
loading spinner
filter filter

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-border-strong | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-info-fg | --aui-color-primary | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-color-warning-fg | --aui-color-warning-subtle | --aui-control-height-sm | --aui-duration-fast | --aui-duration-normal | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-mono | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-medium | --aui-font-weight-regular | --aui-font-weight-semibold | --aui-icon-size-md | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-4 | --aui-space-6

interface DataGridSort { column: string; direction: 'ascending' | 'descending' }
interface DataGridState { version: 1; sort: readonly DataGridSort[]; columns: DataGridColumnsState; page: number; pageSize: number | null; filters?: DataGridFilters; quickFilter?: string; advancedFilter?: DataGridAdvancedFilter | null; selected?: readonly string[]; groupBy?: readonly string[]; expanded?: Readonly<Record<string, boolean>> }
interface DataGridCellRange { startRow: string; endRow: string; startColumn: string; endColumn: string }
interface DataGridGroup { id: string; column: string; key: unknown; count: number; aggregates: Record<string, unknown>; children: DataGridGroup[] }
interface DataGridCellChange { rowId: string; column: string; oldValue: unknown; newValue: unknown }
interface DataGridCellEditor { element: HTMLElement; getValue(): unknown; focus(): void; control?: HTMLElement; popup?: boolean; destroy?(): void }
interface DataGridRowSelection<Row> { mode: 'single' | 'multiple'; checkboxes?: boolean; headerCheckbox?: boolean; selectAll?: 'filtered' | 'all' | 'page'; clickToSelect?: boolean; selectable?: (row: Row) => boolean }
interface DataGridAdvancedFilter { join: 'and' | 'or'; items: readonly (DataGridAdvancedCondition | DataGridAdvancedFilter)[] }
interface DataGridAdvancedCondition { column: string; filter: DataGridFilter }
type DataGridFilter = DataGridTextFilter | DataGridNumberFilter | DataGridBigIntFilter | DataGridDateFilter | DataGridBooleanFilter | DataGridSetFilter;
interface DataGridTextFilter { kind: 'text'; join?: 'and' | 'or'; conditions: readonly { operator: DataGridTextOperator; value?: string }[] }
type DataGridColumnDef<Row> = DataGridColumn<Row> | DataGridColumnGroup<Row>;

Pattern: WAI-ARIA APG Grid pattern: one tab stop, roving focus between cells and headers, arrow-key navigation, widgets inside cells reached with Enter or F2.

Key Context Action
Arrow keys Cell or header Move one cell; Up from the first row enters the headers; Down from the headers enters the first row.
Home / End Cell First / last cell of the row.
Ctrl+Home / Ctrl+End Cell First cell of the first row / last cell of the last row.
PageUp / PageDown Cell Move a page of rows.
Enter / F2 Cell with widgets Move focus into the first widget; Escape or F2 returns to the cell.
Enter Cell without widgets Activates the cell (aui-cell-activate).
Enter / Space (Shift adds) Sortable header Sorts (Shift: multi-sort).
Enter / Space Collapsible group header Expands or collapses the group.
Shift+ArrowLeft / Right Header Moves the column or group (announced).
Alt+ArrowLeft / Right (Shift: larger steps) Header Resizes the column by 10 px (50 px), announced.
Tab / Shift+Tab Any Leaves the grid (one tab stop).
Element Role
grid container grid (aria-label, aria-rowcount, aria-colcount)
header and body containers rowgroup
rows row (aria-rowindex)
header cells columnheader (aria-colindex, aria-sort, aria-colspan for groups, aria-rowspan for spanning leaves, aria-expanded for collapsible groups)
cells gridcell (aria-colindex)
spacers, fillers, resize handles, drop indicator none (aria-hidden)
Attribute Element Value
aria-rowcount / aria-colcount grid Full counts (header rows plus displayed rows; visible columns), including rows and columns not rendered
aria-rowindex / aria-colindex rows / cells 1-based positions in the full grid
aria-sort primary sorted column header ascending | descending; other sort levels are spoken as “sort priority n”
aria-busy grid true while loading
aria-describedby grid The description option

One tab stop on the focused cell or header (initially the first header). The focused row and column stay rendered while scrolled away, so focus is never lost to virtualization.

Criterion Level Note
1.3.1 Info and Relationships A
1.3.2 Meaningful Sequence A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.10 Reflow AA
1.4.11 Non-text Contrast AA
1.4.13 Content on Hover or Focus AA
2.1.1 Keyboard A
2.1.2 No Keyboard Trap 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.

  • Sort changes, column moves and resizes, group toggles, and loading are announced through the shared polite live region.
  • Pinned columns are sticky cells in DOM order, so reading order matches the visual order in both directions.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver, TalkBack) is pending.
  • Pointer resizing needs a drag; keyboard and API alternatives exist (WCAG 2.5.7).

default, empty, no matching rows, loading, cell focused, header focused, sorted, multi-sorted, filtered, filter panel open, advanced filter error, builder open, rows selected, cell ranges, editing, edit invalid, row editing, batch pending, grouped, group expanded, column resizing, column dragging, group collapsed, paged.

From Trigger To
unsorted header click or Enter ascending, then descending, then unsorted (tri-state)
sorted Shift+click or Shift+Enter on another header multi-sorted (added with the next priority)
expanded group Enter or click on a collapsible group header collapsed (children with visibleWhen switch)
any page Pagination another page (aui-page-change), scrolled to the top
unfiltered a value typed in the panel, a floating filter, or the search box (after filterDelay; Enter at once), or a choice made filtered (aui-filter-change, row count announced), first page, scrolled to the top
filtered Clear filter, an emptied value, or all set values selected unfiltered
panel closed filter button, Alt+ArrowDown on a header or floating cell, Enter on a read-only floating summary panel open (focus on its first control)
panel open Escape closed, focus back on the header (or the floating control that opened it)
panel open focus or pointer outside the panel closed, pending typed value applied
row not selected checkbox, Space, or (clickToSelect) click selected (aui-selection-change, count announced); single mode deselects the previous row
rows selected Shift+checkbox, Shift+Space, or Shift+click the range from the last toggled row is added
any header checkbox, Space on the selection header, or Ctrl+A every selectable row in the Select all scope selected (or cleared)
no range press and drag over cells, Shift+arrows, or Ctrl+A (cell selection) a cell range (aui-range-change; size announced)
ranges Ctrl+drag / Shift+click / a plain arrow key another range added / the active range extended / ranges cleared
any Ctrl+C on a cell ranges (stacked), else selected rows, else the focused cell copied; announced
editable cell Enter, F2, a printable key (replaces the value), Backspace (empty), double-click, or (clickToEdit) a click editing
editing Enter / Tab, Shift+Tab / Escape / focus leaving committed (focus back on the cell; enterMovesDown moves down) / committed and the next or previous editable cell edited / cancelled / committed
editing an invalid value committed blocked: message under the cell, aria-invalid, announced (invalidEdit: 'revert' discards instead; focus leaving discards)
group collapsed ArrowRight, Enter, a double-click, or a click on its toggle expanded (aui-row-group-toggle; “Eng expanded, 3 rows”)
group expanded ArrowLeft, Enter, a double-click, or a click on its toggle collapsed
details collapsed Enter or ArrowRight in the Details column, or a click on the toggle expanded (aui-detail-toggle; “Details of Acme expanded”)
details expanded Enter or ArrowLeft in the Details column, or a click on the toggle collapsed
row groups the group panel’s picker, its pill buttons, or dropping a groupable header on it regrouped (aui-group-change; “Grouped by Department, City”)
editing with ranges Ctrl+Enter the value written to every editable cell of the ranges (bulk edit)
row mode any start key on a row’s cell editors in every editable cell of the row; Tab and Shift+Tab move between them (past the last or first, the row commits and the next or previous row edits); Enter commits the row (one write) after validateRow
batch startBatch(), then writes pending values shown (marked and spoken “edited”); commitBatch() applies them (one write, one undo step); cancelBatch() discards them
any Ctrl+Z / Ctrl+Y or Ctrl+Shift+Z the last write undone / redone (cells focused and selected)
cells Ctrl+V / Ctrl+X / Delete / Ctrl+D / Ctrl+R / fill handle editable cells written as new row objects in one transaction (aui-cells-change; count announced); with readOnlyEdits only requested
expression edited Enter or Apply with a valid expression advanced filter applied (aui-filter-change)
expression edited Enter or Apply with a syntax error error shown with its position, the offending text selected, announced; nothing applied
builder closed Builder builder open (modal), focus on the root group’s combine choice
builder open Apply / Cancel or Escape applied / discarded; focus back on Builder
  • One tab stop with roving focus (WAI-ARIA APG Grid). Home/End follow the APG (row), and Tab leaves the grid.
  • The focused row and column stay rendered while scrolled away, so virtualization never loses focus.
  • Sort changes (“Sorted by Age, descending; City, ascending”), column moves (“Name moved to position 2 of 5”), resizes, group toggles, and loading are announced through the shared polite live region.
  • Pinned columns are sticky cells in DOM order, so the reading order matches the visual order in both directions.
  • Syntax errors are announced assertively with their position (“Unknown column "Town" at character 16”) and the offending text is selected so it can be retyped at once.
  • Filter results are announced politely (“2 rows”); typed values apply after filterDelay so each keystroke is not announced. Filter state is spoken with the header and shown by a dot on the filter button as well as its color (not color alone).
  • Pointer resizing needs a drag; Alt+arrows and the state API are the alternatives (WCAG 2.5.7).
  • Manual screen-reader verification is pending.

layout: 'fill' fills the host (give the host a height); layout: 'auto' grows with its rows up to the host’s max-block-size. Flex columns share the viewport width and re-flow when it changes. The header stays on top and pinned columns stay at their edges while the grid scrolls in both axes.

Logical properties: pinned start columns sit on the right in right-to-left layouts, arrow keys and resizing mirror, and the drop indicator follows the visual edge.

Token-driven. In forced-colors mode, focus outlines and resize lines use Highlight and borders use CanvasText.

configureAbsoluteUI({ components: { dataGrid: { multiSort: false, overscan: 12 } } });

Instance options override global and theme defaults. All configurable options are dynamic.

datetime columns are formatted in the configured IANA time zone (timezone), never the machine’s zone.

icons.sortAscending (arrow-up), icons.sortDescending (arrow-down), icons.sortNone (arrow-up-down), icons.expand (chevron-right, mirrored in RTL), icons.collapse (chevron-down), icons.loading (spinner), icons.filter (filter), icons.drag (grip, the row drag handle).

Cell text uses textContent; render returns text or consumer-owned Nodes; cellClass tokens are validated class names. Filter models from state, URLs, or the UI are validated (DATA_GRID_FILTER) and matched as data; filter values are never parsed as code or HTML. Expressions are read by a hand-written tokenizer and recursive-descent parser (no eval, no Function), with limits on nesting (8 levels) and size (500 conditions). Sizes and offsets are written through the CSSOM, so a strict Content Security Policy with Trusted Types works. No string expressions are evaluated.

No persistence, network access, or telemetry. getState() returns JSON the application may store; setState() treats it as untrusted.

Row offsets come from a Fenwick tree (O(log n) per lookup or height change; uniform heights need no per-row storage); only the visible rows plus overscan and the visible center columns are rendered; rows and cells are reused by id across renders; scroll updates run once per animation frame. Text sorting ranks each column once with the locale collator and then compares numbers, so direction changes and multi-sorts on the same rows are fast. Filters compile once per change; text conditions share one normalized text per row, and the quick filter formats a row’s columns only until one matches, caching the texts per row. Set filter values are extracted once and cached until the rows change. Above about 10 million pixels of content, the scroll space is scaled so very large row counts still scroll smoothly. Budgets: metadata/data-grid/component.json, measured in tests/e2e/perf-data-grid.spec.ts.

Tested with 1,000,000 rows and 300 columns.

  • Hosts without a height in layout: 'fill' collapse to the header; give the host a height or use layout: 'auto'.
  • rows are never mutated; setRows keeps the sort, focus, and scroll position. Transactions never mutate the given rows either; the grid keeps its own row list (read it back with getState or keep your own copy).
  • Change highlighting compares the displayed text, so a value that changes but formats the same (for example a rounding difference) is not highlighted. Highlights are not announced per cell; aui-rows-change carries the changed ids for applications that announce.
  • A column group split by pinning shows one group header per section.
  • Hiding the focused column moves the tab stop to the first header.
  • Selection is kept by row id across sorting, filtering, paging, and updates; removing selected rows reports them in aui-selection-change (reason: 'rows'). Select all covers the rows that pass the filters by default (selectAll: 'all' includes filtered-out rows; 'page' only the current page). The checkbox column (aui-row-select, a reserved id) is pinned first and is not part of the column state.
  • Ranges are kept by row and column ids, so they follow sorting and column moves as rectangles of display positions; a range whose corner is filtered out, paged away, or hidden is not shown until it is displayed again. Ranges never include the checkbox column and are not part of the saved state. Ranges always contain the focused cell: moving focus outside them (Tab, a plain arrow key, focusCell) clears them.
  • Writes never mutate rows: a writable cell (editable, plus setValue, or a field on a plain-object row) gets a new row object (setValue, or a shallow copy with the field replaced) and the changed rows are applied as one transaction (sort, filters, flash, focus, and scroll position stay consistent). setValue must keep the row id. Read-only cells, the checkbox column, and unchanged values are skipped.
  • Pasted text is converted with the column’s parseValue, else by type in the grid’s locale: numbers with the locale’s separators (currency symbols ignored, parentheses negative), dates as ISO or in the locale (numeric or with month names), booleans as the grid’s Yes/No words or true/false, date-times only as ISO instants; empty text writes a blank. Text that does not convert leaves the cell unchanged and is reported. A block that divides the active range evenly is tiled over it; otherwise it is pasted at the range’s top-left cell and rows or columns beyond the grid are dropped.
  • Editing: a cell is editable when its column is writable (editable plus setValue, or a field on plain-object rows). Edits go through the same write path as paste (new row objects, one transaction, aui-cells-change with source: 'edit'; with readOnlyEdits only requested). The number editor reads the grid’s locale and rejects letters; the date-time editor shows and reads wall-clock time in the grid’s IANA time zone (a time inside a daylight-saving gap is rejected); the select editor’s options can be a function of the row. Built-in validation: required, number min/max, maxLength, date min/max (ISO), and values an editor cannot convert; then the column’s validate. An editor whose row is filtered out or removed is discarded.
  • Date hierarchies: a date or date-time column with groupHierarchy groups by each part as its own level (in the grid’s time zone; ISO weeks), labeled “2026”, “Q1 2026”, “January 2026”, “Week 3, 2026”, or the formatted day. showAs shows a numeric aggregate as a percentage of the grand total or of the parent group (top-level groups use the grand total); aggregateFilteredOnly: false aggregates every row of a group, including rows the filters hide.
  • Tree data: rows form a tree from paths (getPath; missing levels become filler rows named by their path element), parent ids (parentIdField; missing parents and cycles make a row a root, reported by getTreeOrphans(), never looped over), or nested rows (childrenField, read when rows are set; rows added by transactions are roots). The group column shows the tree (label names the column that labels rows; paths use their last element); groupBy is ignored. A parent with its own row stays a data row: editable, selectable, and copied as usual, with aggregated columns showing the aggregate of the rows below it. Siblings follow the sort; filtering keeps the ancestors of matching rows, and treeFilter: 'subtree' also keeps the rows below a matching row. On a parent row, ArrowRight, ArrowLeft, Enter, and a double-click expand and collapse in the group column; elsewhere in the row they act as on a data row.
  • Grouping: groups form over the filtered, sorted rows; groups are ordered by key (by the grouped column’s sort direction when it is sorted; blanks last as “(Blanks)”); rows keep their sort within a group. Aggregates are computed from each group’s rows (an average is never an average of averages; BigInt sums stay BigInt); counts show as plain numbers, other aggregates in the column’s format. Group and total rows are not editable, filled, pasted into, or selected; copy reads their labels and aggregates. Group ids are stable for the same keys, so expansion (in the state) survives updates. Paging pages over the displayed rows (group rows included).
  • Row mode (editMode: 'row'): every editable visible cell of the row gets an editor (large text renders in the cell); the row is outlined; per-cell validation runs first, then validateRow(values, row), whose message (or messages by column) blocks the commit and focuses that cell’s editor. A committed row is one transaction, one aui-cells-change, and one undo step.
  • List editors live in a separate entry point so the grid itself does not include Combobox and Multi Select: comboboxEditor({ values | loadValues, loadDelay, placeholder }) edits one value with typeahead filtering or values loaded per query (aborted by newer queries, loading and errors announced); multiSelectEditor({ ..., maxSelected, selectAll }) edits an array of values with chips. Choice values are strings, numbers, or booleans; values can be a function of the row (dependent lists). The component’s label is the editor’s name (“{column}, {row}”), visually hidden in the cell. Inside the list, Enter chooses an option and Escape closes the list; with the list closed, Enter commits and Escape cancels.
  • Batch editing: between startBatch() and commitBatch(), writes from editing, paste, cut, Delete, and fill become pending values: cells show them in italics with a corner mark and the cell’s name ends with “, edited”; copy reads them; sorting and filtering keep the committed data; events and undo come with the commit (source: 'batch'). Undo during a batch is not available (cancelBatch() discards it). Rows the application replaces or removes during a batch drop their pending values.
  • Undo: each applied write is one step (undoLimit, default 20); replacing or updating rows from the application (rows, setRows, applyTransaction) clears the history because old values may be stale; nothing is recorded with readOnlyEdits.
  • Fill: two or more numbers or dates continue their linear series (dates by calendar days, or by working days when every source date is a working day of the column’s business calendar); a single value, text, and mixed values repeat; Alt repeats a series or counts up from a single value. Dragging back into the range clears the cells it leaves; double-clicking the handle fills down to the last displayed row. Ctrl+D and Ctrl+R copy (no series).
  • Copy uses the displayed text (formatted in the grid’s locale and time zone) or the column’s copyValue; values with tabs, line breaks, or quotes are quoted. Keyboard copy uses the native copy event (no permission prompt); text the user selected inside a cell copies natively.
  • Text filters match the displayed text, case- and accent-insensitively by default; incomplete conditions (an operator without a value) filter nothing.
  • A set filter lists the distinct displayed values of all rows (not only the filtered ones), blanks first as “(Blanks)”, numbers and dates in value order; the panel renders at most 1,000 matching values and asks for a narrower search. Selecting every value removes the filter.
  • The quick filter matches the displayed text of visible searchable columns (or filterValue when given); set searchable: false on columns that should not be searched.
  • Floating filters edit simple filters in place (one condition the compact control can express); other filters show a read-only summary, and Enter on it opens the panel.
  • The advanced filter replaces the column filter UI (header buttons and floating row) and the column filters stop applying while it is on; they are kept and apply again when it is turned off. The quick filter and the external filter still apply.
  • Advanced expressions: columns by header in brackets (]] for a ] in a header; a column id also works), text and dates in double quotes (\" and \\ escape), numbers bare (. decimal separator), and binds tighter than or, parentheses group. Keywords are English and case-insensitive. Operators: text contains, does not contain, =, !=, starts with, ends with; numbers =, !=, <, <=, >, >=, between 1 and 5; dates the same symbols (or on, before, after, on or before, on or after), is today, is this week (and the other relative ranges), is year to date, is in last 7 days, is in next 7 business days, is working day, is holiday; every kind is blank, is not blank; booleans is true, is false; set columns is any of ("Oslo", blank) (or in (...)). An empty expression clears the filter.
  • The builder lists at most 1,000 values of a set column (values already chosen stay listed); the expression bar accepts any value.
  • The floating number syntax uses . as the decimal separator: 5, >5, >=5, <5, <=5, !=5, 5..10.
Code Category Cause
DATA_GRID_OPTIONS configuration Missing label, columns, rows, or rowId; malformed column or group; duplicate ids; invalid sizes or option values. Messages name the property, never the value.
DATA_GRID_ROW_ID configuration Empty or duplicate row id.
DATA_GRID_STATE configuration Malformed setState input (the grid is left unchanged).
DATA_GRID_FILTER configuration Malformed filter model in filters, setFilter, advancedFilterModel, setAdvancedFilter, or setState (unknown operator, wrong value type, too many conditions, nesting deeper than 8); the message gives the property path, never the value.

Syntax errors in the expression bar are not thrown: they are shown, announced, and leave the applied filter unchanged.

  • Menus: the column menu offers sorting (Clear sort while sorted), the filter, pinning, grouping, hiding, and two submenus: Groups (expand and collapse all, while grouped) and Columns (autosize this or all columns, rename, the column chooser, reset columns); the cell menu offers copying (with or without headers), cutting, pasting (where the Clipboard API is available), pinning the row, and the Groups and Columns submenus. Entries that do not apply are left out, which keeps the menus short enough to show without scrolling. The column chooser (columnChooser(), opened from Columns > Choose columns or with its open()) shows, hides, and reorders columns at once (aui-columns-change); search filters its list. columnMenu({ items(column, defaults) }) and contextMenu({ items(target, defaults) }) change the entries: built-in names, 'separator', or your own { id, label, action, disabled, shortcut, icon, items }. Actions taken from a menu behave like their keyboard and pointer counterparts (sorting from the menu emits aui-sort-change). Outside headers and cells the browser’s menu stays. Accessories attach when the grid is created (the grid must be in the document).
  • Tooltips: a column’s tooltip (true shows the displayed text) for data rows; tooltips({ showDelay, mode: 'truncated', trigger: 'hover' }) shows them only for text that does not fit, or only on hover. Find: matches are cells of the displayed rows (all pages; rows of collapsed groups are not searched) whose displayed text contains the text (caseSensitive option); the current one is scrolled into view and its page shown; matches follow data changes; the API is find(text), next(), previous() on the accessory. Notes are stored by your application (notes({ store: { get, set, index }, author }); the grid stores nothing): index() marks the cells that have notes, saves write { text, author, updated }, an empty note deletes.
  • Side bar (sideBar({ panels, position, open, width })): Columns shows the column list (search, Show all, per column visibility, Group toggles for groupable columns, Move up / Move down); Filters shows each filterable column’s filter editor in place (changes apply as in the filter popup). The side bar sits beside the rows (position start or end). Pivot rows, columns, and values are edited in the pivot panel.
  • Status bar (statusBar({ panels, format })): row counts (“Rows: 120 of 500” while filtered; the server count with a datasource), the selected row count, and while the cell ranges cover two cells or more their count of filled cells and the sum, average, minimum, and maximum of their numbers; panels of your own ({ id, render(context) }) refresh after every change. Toolbar (toolbar({ items, label })): built-ins ('chooser', 'resetColumns', 'autoSizeAll', 'expandAll', 'collapseAll', 'separator') and buttons of your own ({ id, label, icon, action }, or items for a menu) next to the search box and the group and pivot panels.
  • Row spanning (spanRows) merges adjacent data rows whose displayed values are equal and not blank (or that the function calls equal), at most 500 rows per span; group, total, and full-width rows break spans, and sorting and filtering recompute them. Column spanning (colSpan(row)) covers the next columns of the same pinned section (not with spanRows on the same column). Spans are visual: copies, ranges, and writes still address every cell by its column.
  • Full-width rows (fullWidthRow) draw render(row) across the row; their content stays in view while the rows scroll sideways and is released through the render-release contract when the row leaves; they are never edited, filled, or part of ranges. groupDisplay: 'rows' draws group rows that way (no group column; group totals then name their group in the first data column). Sticky group rows follow the topmost row while grouped (stickyGroupRows: false turns them off).
  • Pinned rows: static rows (pinnedTopRows, pinnedBottomRows) are never sorted, filtered, grouped, selected, or edited; rows pinned by id stay in the body too, follow its filters (a filtered-out row leaves its section), and are read-only in their section. grandTotal works on flat grids too (labeled “Total” in the first data column), and pinnedTop / pinnedBottom keep it in a pinned section. Pinned sections stay under the header and at the bottom edge while the body scrolls; rows hidden under them count as off screen when focus moves.
  • Printing builds a real table (caption, th with scope, header rows repeated per page) so printed and saved-as-PDF output keeps its structure; while printing, every other child of body is hidden. Print colors use system colors in a light scheme, whatever the theme.
  • Formulas live in @absoluteui/components/data-grid/formulas (our own parser and evaluator; never eval, so strict CSP and Trusted Types stay intact). Pass formulas: formulaEngine({ functions, calendar }) and give columns a formula such as [Price] * [Quantity]: [Column] (column id or header, without case) is the same row’s value, or the whole column inside functions (SUM([Total])); [@Column] is always the same row; A1 references use stable addresses (letters by column definition order, numbers by source row order). Functions: math and aggregates (SUM, AVERAGE, MIN, MAX, COUNT, COUNTA, COUNTBLANK, PRODUCT, ROUND, ROUNDUP, ROUNDDOWN, ABS, INT, MOD, POWER, SQRT, SIGN), conditional (SUMIF, SUMIFS, COUNTIF, COUNTIFS, AVERAGEIF; wildcards * and ?), logic (IF, IFS, IFERROR, IFNA, AND, OR, XOR, NOT, SWITCH, CHOOSE, ISBLANK, ISNUMBER, ISTEXT, ISERROR), text (CONCAT, TEXTJOIN, LEFT, RIGHT, MID, LEN, UPPER, LOWER, PROPER, TRIM, FIND, SEARCH, SUBSTITUTE, REPT, VALUE, TEXT), dates (TODAY in the grid time zone, DATE, YEAR, MONTH, DAY, WEEKDAY, DAYS, EDATE, EOMONTH, NETWORKDAYS and WORKDAY on the engine’s business calendar), lookups (XLOOKUP, INDEX, MATCH, VLOOKUP). Formulas use . for decimals and , between arguments whatever the locale. Calculated columns are read-only, sort, filter, group, aggregate, and export their results (CSV and Excel write values). Results recalculate when rows change; cycles give #CIRCULAR!; limits (8192 characters, 64 nesting levels, 1,000,000 cells per range, 1,000,000 evaluation steps) give errors, never hangs. Error cells show the code and read its meaning (“#DIV/0!, division by zero”). Columns with formulas: true hold cell formulas in the row data (a value that is text starting with =): the cell shows the result, the editor shows the formula (a text editor; other input is read as the column type), invalid formulas are refused with the position of the problem, and fill, Ctrl+D / Ctrl+R, and pasting cells copied in the grid move relative A1 references ($ parts stay); text pasted from elsewhere is kept as typed. The editor of a formulas cell is a combobox: typing a function name or [ lists matching functions and columns (ArrowUp and ArrowDown move, Enter or Tab accepts, Escape closes the list), the signature of the function around the caret shows under the field and is part of its description, a syntax problem shows there after a pause, and while a formula is edited a click on another cell inserts its address where an operand is expected (keyboard users type references, helped by the suggestions). The formulaBar() accessory (from data-grid/formulas) shows the focused cell’s address and formula above the grid (read-only).
  • Charts live in @absoluteui/components/data-grid/charts (outside the core bundle, drawn by AbsoluteUI chart components). Sparklines in cells: spread sparklineColumn({ values, label, variant, color, min, max, valueFormat, sortBy, locale }) into a column; each cell draws a Sparkline named “{label}: {count} values, from {first} to {last}, …”, copy and exports get the numbers as text, and sorting compares the last value (or sum, min, max). Load the Sparkline styles. Class: aui-data-grid__sparkline. Range charts: chartRange(grid, { type, seriesBy, title, range }) opens a panel docked below the grid (a region named “Chart of {title}”, resizable, Escape or Close closes it and focus returns to the grid) with a bar, stacked bar, line, area, or pie chart of the selected cells (the first text column names the categories; numeric columns, or rows with seriesBy: 'rows', are the series); it follows edits, transactions, sorting, and filtering, and each chart has its own data table. chartPivot(grid, { type, title, level }) does the same for pivot results (the rows of a group level against the value columns). Both return { element, update, close }; load the Bar, Line, and Pie Chart styles. Classes: aui-data-grid-chart, __tools, __field, __close, __body, __chart, __empty. To offer it in the cell menu, add an item: contextMenu({ items: (target, defaults) => [...defaults, { id: 'chart', label: 'Chart range', action: () => chartRange(grid) }] }).
  • For tools (for example an AI assistant), @absoluteui/components/data-grid/schema has dataGridStateSchema(grid) (a JSON Schema, draft 2020-12, of the view state setState accepts, with the grid’s column ids, filter kinds, groups, and pivot aggregates), describeState(grid) (a short English summary of the current view), and applyState(grid, state) ({ ok: true } or { ok: false, error } with the validation message, so a tool can correct its proposal). Only view state: no row data, no edits, no row ids; setState validates everything a tool proposes.
  • PDF export lives in @absoluteui/components/data-grid/pdf: toPdf(grid, options) returns a Promise of a PDF Blob with the toCsv row and column choices, the title on the first page, column group and header rows repeated on every page, bold group and total rows, end-aligned numbers, and “Page n of N” (pageLabel); options pageSize (a4, letter), orientation (default landscape), margin, fontSize, rightToLeft, columnGroups. Column widths follow the grid, scaled down to fit the page; long text is cut with an ellipsis. Latin text (Windows-1252) uses the standard Helvetica fonts; for other scripts pass fonts: { regular, bold } with TrueType or OpenType bytes (embedded whole, with a ToUnicode map so text can be copied and searched; fonts whose license forbids embedding are refused; font collections are not read). Right-to-left grids mirror the columns and order bidirectional text in a simplified way; Arabic letters are not shaped, so use the browser’s Save as PDF through print() for Arabic and other complex scripts. The PDF is not tagged (no PDF/UA structure): for assistive technology prefer the grid, CSV, or Excel. More than 100,000 rows throws. The cell menu offers PDF with contextMenu({ export: { pdf: (g) => toPdf(g) } }).
  • Excel export lives in @absoluteui/components/data-grid/export (outside the core bundle): toXlsx(grid, options) returns a Promise of an .xlsx Blob; xlsxSheet(grid, options) and writeXlsx({ sheets }) build workbooks with several sheets (grids or plain rows). Options: the toCsv row and column choices, sheetName, header, columnGroups (merged), styles (named styles for excel.style and excel.cellStyle), headerStyle, groupStyle, totalStyle, freeze (header rows and start-pinned columns, default on), outline (group and tree levels, collapsed like the grid, default on), autoFilter, dateFormat, dateTimeFormat (in the grid time zone), protection (password, spinCount, allow), rightToLeft, title, creator. Values are typed (numbers, booleans, dates as Excel dates); text is always written as text, so formula-like values cannot run; formulas come only from excel.formula code. Links keep http, https, and mailto only; images must be PNG or JPEG bytes. A protection password is hashed with SHA-512, a random salt, and spinCount rounds (default 100000, which takes a few seconds); it is never stored. Excel limits apply: 1,048,576 rows, 16,384 columns, 32,767 characters per cell (longer text is cut). Pass export: { excel: (g) => toXlsx(g) } to contextMenu for the cell menu’s Excel item.
  • CSV export escapes formula-like text (=, +, -, @, tab, carriage return) with a leading quote, including group labels when the indent starts with one of them; numbers the grid formats itself are not escaped, value callbacks returning strings are. The cell menu (contextMenu accessory) has Export > CSV (and Excel with export: { excel }), named after the grid’s label; export: false removes it.
  • State: getState() includes details, pinnedRows, pivot, and columns.headers; setState restores them (a malformed pivot throws DATA_GRID_STATE before anything changes; unknown pinned ids are dropped). Aligned grids share column state by column id: columns only one grid has are left as they are.
  • Auto-generated columns: inferColumns(rows, { sample, exclude }) returns columns for the rows’ own keys (number, boolean, date for PlainDate, datetime for Instant or ZonedDateTime, else text; keys holding only objects are left out). For a dropped CSV file: parseCsv(text, { header: true }) from @absoluteui/helpers/csv, then coerceCsvValues(rows) (numbers and true/false become typed; numbers with leading zeros stay text), then inferColumns.
  • Managed row dragging moves client rows in source order only: flat rows that are not sorted, filtered, grouped, or pivoted, or tree data with parentIdField (moves keep row objects and copy a moved row with its new parent id). Otherwise, and without managed, the grid only emits aui-row-drop. A move is one undo step; moveRow clears the undo history. Rows dropped from another grid are added with a transaction (it clears the undo history), only to client rows. With nativeData the browser draws the drag and pointer edge cases follow native drag and drop (no touch drag where the browser has none; the menu still works).
  • Row numbers count displayed rows (sorting, filtering, grouping, and paging renumber them); with cell ranges a click on a number selects the row’s cells; numbers are never copied, pasted into, or filled. Row heights set by the user or setRowHeight stay with the row id (not with autoRowHeight, where the resizer is off).
  • Server row model (datasource): getRows({ start, end, sort, filters, quickFilter, advancedFilter }, signal) returns { rows, rowCount? }; blocks of blockSize rows load as they scroll into view (the topmost first, maxConcurrentLoads at a time, debounced by blockLoadDelay; stale requests are aborted). Without rowCount the total grows a block at a time until a block comes back short. A sort, filter, quick filter, or advanced filter change purges the cache and reloads from the top (the previous count stays as an estimate). Failed blocks are never retried automatically (retryLoads(), or Retry). maxBlocks releases the least recently used blocks. applyTransaction updates loaded rows; applyServerTransaction also adds and removes loaded rows (later rows shift; blocks left incomplete load again when shown). Edits, paste, fill, and undo write loaded rows. Selection keeps the ids of rows that are not loaded; the header checkbox selects every row, loaded or not (getServerSelection() returns { selectAll: true, toggled }, toggled being the exceptions), and getSelected() lists loaded rows only. Server groups: with groupBy the request carries groupBy and groupKeys; the top level returns group rows (ordinary rows: the key is read from the grouped column, aggregates from aggregated columns, the count from getChildCount(row)), and each expanded group loads its own rows (blocks, loading and failed rows, and paging as at the top level) with groupKeys set to its path; loading rows of a group are sized by its count. Collapsed groups keep their rows unless purgeClosedGroups. Server tree data: treeData: { serverGroup(row), serverKey(row), label }; a parent’s children load with groupKeys holding the keys of its ancestors and itself. expandAll() expands the loaded groups; group rows have no group checkbox (their rows are not all loaded). Not combinable: pivot, detail, and client tree modes (rejected). Set filters list filterValues, else the values of loaded rows.
  • Pivot mode: rows group by the row fields (sorted by key like groups; the last level has no data rows), and the Pivot Grid engine aggregates the rows that pass the filters for every (row, column) key pair: a column group per column key (nested per column field, labeled like the column) holds a result column per value (“Sum of Salary”), then “Total” columns. Sums, averages, minimums, and maximums read numbers (currency columns keep their currency); counts count non-blank values and distinct counts compare values (dates by day). The data columns hide while pivoting; generated columns are not sortable or movable. Without row fields one row shows the grand totals; grandTotal adds a total row otherwise. More than pivotMaxColumns result columns are left out (announced; getPivotTruncation()). treeData takes precedence over pivot, and pivot over groupBy.
  • Master/detail: detail rows follow their master rows (paging counts master rows only; a page change keeps them with their masters), are not selectable, editable, or part of copies, and do not scroll sideways. Content renders once and is kept while expanded, and for up to keep collapsed details (oldest released first, through destroy); a replaced row object renders it again. Heights are fixed (default 300) or measured (auto).
  • Not available yet: sorting pivot rows by a result column, and pivot mode over a datasource.
  • Column filter changes apply live; there is no Apply button mode yet.
  • Advanced filter keywords (and, contains, is any of, …) are English; column headers and messages are localized.
  • Children stay within their column group when moved.
  • Pointer resizing needs a drag; keyboard and API alternatives exist.
  • Manual screen-reader verification is pending.
  • HTML strings in cells.
  • String expressions (only functions).