Data Table
import { DataTable } from '@absoluteui/components/data-table';import '@absoluteui/components/pagination/style.css';Using Angular? See Data Table for Angular.
What it does
Section titled “What it does”Show records that users scan, compare, sort, filter, select, and page through: an enterprise CRUD table with grid features. DataTable renders a real <table> with a <caption>, column headers (th scope="col"), and a row header per row (th scope="row"). Cells can be typed (number, currency, date, datetime in the configured timezone, boolean) and are formatted for the locale. Sorting is stable, by one column or (since 0.3.0) several. Selection uses native radios (single) or checkboxes (multiple, with a per-page “Select all rows” and Shift+click ranges). Paging composes Pagination with a result summary. In client mode the table filters, sorts, and pages rows itself. In server mode rows is the current page, and sorting, filtering, and paging only emit events.
Since 0.3.0, every one of these is opt-in and off by default, so a table without them renders exactly as before:
| Feature | Enable with |
|---|---|
| Multi-column sorting (Shift+click, Shift+Enter) | multiSort: true, or a sort array |
| Column filters (text, number, date, enum) and a server filtering contract | column filter (+ filterOptions), filters, aui-filter-change |
| Global search | searchable: true, search |
| Column visibility and column chooser | hiddenColumns, DataTableColumnChooser.create(button, table) |
| Column resize (pointer and keyboard) | resizable: true, columnWidths, column minWidth/maxWidth |
| Column pinning (sticky start/end) | pinnedColumns: { start, end } |
| Column order | columnOrder |
| Row expansion (detail rows) | renderDetail, isRowExpandable, expanded |
| Virtual scrolling | virtual: true |
| CSV export | toCsv(), toCsvBlob(), downloadCsv() |
| State serialization | getState(), setState() |
It is deliberately not an ARIA grid. Users Tab through the search field, sort buttons, resize handles, filter fields, selection inputs, disclosure buttons, and interactive cell content. Arrow-key cell navigation, editing, and cell selection are not provided.
Design decision (0.3.0): the grid features extend Data Table instead of adding a separate Data Grid subpath. The options, events, and markup of 0.2.0 are unchanged (the sort event gained a sorts field), there is one component to learn, and server/client modes work the same for every feature. The column chooser is a separate class, so a table without it does not bundle Menu.
When to use
Section titled “When to use”- Lists of records users scan, compare, sort, select, and page through (orders, users, invoices).
- Server-paged data where the backend sorts, filters, and pages, and the table renders the current page.
- Operational grids that need filters, several sort columns, column choice and widths, pinned identifier columns, detail rows, or CSV export, with the view state saved and restored.
- Large client-side row sets (tested with 100,000 rows) in a fixed-height region with virtual: true.
When not to use
Section titled “When not to use”- Spreadsheet-like interaction with cell-by-cell arrow-key navigation or cell selection: not provided (the table keeps plain table semantics; every control, editable cells included, is reached with Tab).
- Page layout or visual alignment of non-tabular content: use Layout.
- Rows of different heights (wrapping text, detail rows) with virtual scrolling: virtual rows are single-line and uniform; page instead.
- Very large data sets that do not fit in memory: use server mode.
Examples
Section titled “Examples”Client-side sorting, paging, and multiple selection
Section titled “Client-side sorting, paging, and multiple selection”import { DataTable, type DataTableColumn, type DataTableSelectionChangeDetail } from '@absoluteui/components/data-table';import { instant, plainDate, type Instant, type PlainDate } from '@absoluteui/core';
interface Order { id: string; customer: string; total: number; placed: PlainDate; updated: Instant; paid: boolean;}
const orders: Order[] = [ { id: 'o-1001', customer: 'Acme Corp', total: 1250.5, placed: plainDate(2024, 3, 1), updated: instant(Date.UTC(2024, 2, 2, 9, 30)), paid: true }, { id: 'o-1002', customer: 'Globex', total: 310, placed: plainDate(2024, 3, 4), updated: instant(Date.UTC(2024, 2, 4, 16, 0)), paid: false },];const columns: DataTableColumn<Order>[] = [ { id: 'customer', header: 'Customer', accessor: (o) => o.customer }, { id: 'total', header: 'Total', accessor: (o) => o.total, type: 'currency', currency: 'USD' }, { id: 'placed', header: 'Placed', accessor: (o) => o.placed, type: 'date' }, { id: 'updated', header: 'Updated', accessor: (o) => o.updated, type: 'datetime' }, { id: 'paid', header: 'Paid', accessor: (o) => o.paid, type: 'boolean', sortable: false },];const host = document.createElement('div');document.body.append(host);const table = DataTable.create(host, { caption: 'Orders', columns, rows: orders, rowId: (o) => o.id, selection: 'multiple', pageSize: 25, pageSizes: [25, 50, 100], timezone: 'America/New_York', sort: { column: 'placed', direction: 'descending' },});host.addEventListener('aui-selection-change', (event) => { const { selected } = (event as CustomEvent<DataTableSelectionChangeDetail>).detail; host.dataset.selectedCount = String(selected.length);});table.setSelected(['o-1001']);Server mode: load pages and sorted data on events
Section titled “Server mode: load pages and sorted data on events”import { DataTable, type DataTablePageChangeDetail, type DataTableSort, type DataTableSortChangeDetail } from '@absoluteui/components/data-table';
interface User { id: string; name: string; email: string;}
const host = document.createElement('div');document.body.append(host);const table = DataTable.create<User>(host, { caption: 'Users', mode: 'server', total: 0, rows: [], loading: true, rowId: (u) => u.id, columns: [ { id: 'name', header: 'Name', accessor: (u) => u.name }, { id: 'email', header: 'Email', accessor: (u) => u.email }, ],});let sort: DataTableSort | null = null;const load = async (page: number, pageSize: number): Promise<void> => { table.update({ loading: true }); const query = new URLSearchParams({ page: String(page), size: String(pageSize), sort: sort ? `${sort.column}:${sort.direction}` : '' }); const response = await fetch(`/api/users?${query.toString()}`); const body = (await response.json()) as { rows: User[]; total: number }; table.update({ rows: body.rows, total: body.total, loading: false });};host.addEventListener('aui-sort-change', (event) => { sort = (event as CustomEvent<DataTableSortChangeDetail>).detail.sort; void load(table.page, table.pageSize ?? 25);});host.addEventListener('aui-page-change', (event) => { const { page, pageSize } = (event as CustomEvent<DataTablePageChangeDetail>).detail; void load(page, pageSize);});void load(1, 25);Custom cell rendering and a row-header column
Section titled “Custom cell rendering and a row-header column”import { DataTable } from '@absoluteui/components/data-table';
interface Invoice { id: string; number: string; amount: number;}
const host = document.createElement('div');document.body.append(host);DataTable.create<Invoice>(host, { caption: 'Invoices', hideCaption: true, pageSize: null, density: 'compact', rows: [{ id: 'i1', number: 'INV-001', amount: 99.5 }], rowId: (i) => i.id, columns: [ { id: 'number', header: 'Invoice', accessor: (i) => i.number, rowHeader: true, width: '12rem' }, { id: 'amount', header: 'Amount', accessor: (i) => i.amount, type: 'number', format: (value) => `${String(value)} EUR` }, { id: 'open', header: 'Actions', accessor: () => null, sortable: false, render: (invoice, doc) => { const link = doc.createElement('a'); link.href = `/invoices/${encodeURIComponent(invoice.id)}`; link.textContent = `Open ${invoice.number}`; return link; }, }, ],});Since 0.3.0: filters, multi-sort, column chooser, resize, pinning, row details, CSV export, and saved view state
Section titled “Since 0.3.0: filters, multi-sort, column chooser, resize, pinning, row details, CSV export, and saved view state”import { DataTable, DataTableColumnChooser, downloadCsv, type DataTableColumn, type DataTableState,} from '@absoluteui/components/data-table';import { instant, plainDate, type Instant, type PlainDate } from '@absoluteui/core';import { Button } from '@absoluteui/components/button';
interface Ticket { id: string; title: string; status: 'open' | 'closed'; priority: number; due: PlainDate; updated: Instant; description: string;}
const tickets: Ticket[] = [ { id: 'T-1', title: 'Login fails on Safari', status: 'open', priority: 1, due: plainDate(2026, 4, 1), updated: instant(Date.UTC(2026, 2, 20, 9, 0)), description: 'Steps: open /login in Safari 17.' }, { id: 'T-2', title: 'Export button misaligned', status: 'closed', priority: 3, due: plainDate(2026, 3, 15), updated: instant(Date.UTC(2026, 2, 18, 16, 30)), description: 'Seen at 320 px.' },];const columns: DataTableColumn<Ticket>[] = [ { id: 'id', header: 'Ticket', accessor: (t) => t.id, rowHeader: true, hideable: false }, { id: 'title', header: 'Title', accessor: (t) => t.title, filter: 'text' }, { id: 'status', header: 'Status', accessor: (t) => t.status, filter: 'enum', filterOptions: [{ value: 'open', label: 'Open' }, { value: 'closed', label: 'Closed' }] }, { id: 'priority', header: 'Priority', accessor: (t) => t.priority, type: 'number', filter: 'number', minWidth: 80 }, { id: 'due', header: 'Due', accessor: (t) => t.due, type: 'date', filter: 'date' }, { id: 'updated', header: 'Updated', accessor: (t) => t.updated, type: 'datetime', filter: 'date' },];
const toolbar = document.createElement('div');const chooserButton = document.createElement('button');chooserButton.type = 'button';chooserButton.textContent = 'Columns';Button.enhance(chooserButton);const exportButton = document.createElement('button');exportButton.type = 'button';exportButton.textContent = 'Export CSV';Button.enhance(exportButton);toolbar.append(chooserButton, exportButton);const host = document.createElement('div');document.body.append(toolbar, host);
const STORAGE_KEY = 'tickets-table';const table = DataTable.create(host, { caption: 'Tickets', columns, rows: tickets, rowId: (t) => t.id, selection: 'multiple', multiSort: true, searchable: true, resizable: true, pinnedColumns: { start: ['id'] }, timezone: 'America/New_York', sort: [{ column: 'status', direction: 'descending' }, { column: 'priority', direction: 'ascending' }], renderDetail: (ticket, doc) => { const p = doc.createElement('p'); p.textContent = ticket.description; return p; },});DataTableColumnChooser.create(chooserButton, table);
// Restore and save the view state (the table never persists anything itself).const saved = localStorage.getItem(STORAGE_KEY);if (saved !== null) { try { table.setState(JSON.parse(saved) as DataTableState); } catch { localStorage.removeItem(STORAGE_KEY); }}for (const type of ['aui-sort-change', 'aui-filter-change', 'aui-columns-change', 'aui-page-change']) { host.addEventListener(type, () => { localStorage.setItem(STORAGE_KEY, JSON.stringify(table.getState())); });}
exportButton.addEventListener('click', () => { const rows = table.selected.length > 0 ? 'selected' : 'filtered'; downloadCsv(table.toCsvBlob({ rows, bom: true }), 'tickets.csv');});Since 0.3.0: server mode with filters, search, and multi-sort as one query
Section titled “Since 0.3.0: server mode with filters, search, and multi-sort as one query”import { DataTable, type DataTableColumn, type DataTableState } from '@absoluteui/components/data-table';
interface Customer { id: string; name: string; country: string; revenue: number;}
const columns: DataTableColumn<Customer>[] = [ { id: 'name', header: 'Name', accessor: (c) => c.name, filter: 'text' }, { id: 'country', header: 'Country', accessor: (c) => c.country, filter: 'enum', filterOptions: [{ value: 'DE', label: 'Germany' }, { value: 'IN', label: 'India' }] }, { id: 'revenue', header: 'Revenue', accessor: (c) => c.revenue, type: 'currency', currency: 'EUR', filter: 'number' },];const host = document.createElement('div');document.body.append(host);const table = DataTable.create<Customer>(host, { caption: 'Customers', mode: 'server', total: 0, rows: [], loading: true, rowId: (c) => c.id, columns, multiSort: true, searchable: true,});
// One query shape for every user change: sort, filters, search, and page all come from getState().let controller: AbortController | null = null;const load = async (state: DataTableState): Promise<void> => { controller?.abort(); controller = new AbortController(); table.update({ loading: true }); const response = await fetch('/api/customers/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sort: state.sort, filters: state.filters, search: state.search, page: state.page, pageSize: state.pageSize }), signal: controller.signal, }); const body = (await response.json()) as { rows: Customer[]; total: number }; table.update({ rows: body.rows, total: body.total, loading: false });};for (const type of ['aui-sort-change', 'aui-filter-change', 'aui-page-change']) { host.addEventListener(type, () => { load(table.getState()).catch((error: unknown) => { if (!(error instanceof DOMException && error.name === 'AbortError')) table.update({ loading: false }); }); });}void load(table.getState());Since 0.3.0: virtual scrolling of 100,000 client rows
Section titled “Since 0.3.0: virtual scrolling of 100,000 client rows”import { DataTable } from '@absoluteui/components/data-table';
interface Reading { id: string; sensor: string; value: number;}
const readings: Reading[] = Array.from({ length: 100_000 }, (_, i) => ({ id: `r${String(i)}`, sensor: `Sensor ${String(i % 250)}`, value: (i * 37) % 1000 }));const host = document.createElement('div');document.body.append(host);// Only the rows in view (plus an overscan) are in the DOM; the region is 70vh tall by default.DataTable.create<Reading>(host, { caption: 'Sensor readings', rows: readings, rowId: (r) => r.id, pageSize: null, virtual: true, selection: 'multiple', columns: [ { id: 'sensor', header: 'Sensor', accessor: (r) => r.sensor, filter: 'text' }, { id: 'value', header: 'Value', accessor: (r) => r.value, type: 'number', filter: 'number' }, ],});import { DataTable, DataTableColumnChooser, downloadCsv, type DataTableSelectionChangeDetail } from '@absoluteui/components/data-table';import '@absoluteui/components/pagination/style.css';import '@absoluteui/components/menu/style.css';import '@absoluteui/components/data-table/style.css';
interface Invoice { id: string; customer: string; amount: number; overdue: boolean }
const host = document.querySelector<HTMLElement>('#invoices');const deleteButton = document.querySelector<HTMLButtonElement>('#delete-selected');const columnsButton = document.querySelector<HTMLButtonElement>('#columns');const exportButton = document.querySelector<HTMLButtonElement>('#export');if (host && deleteButton && columnsButton && exportButton) { let invoices: Invoice[] = [ { id: 'i-1', customer: 'Acme Corp', amount: 1250.5, overdue: false }, { id: 'i-2', customer: 'Globex', amount: 310, overdue: true }, ]; const table = DataTable.create<Invoice>(host, { caption: 'Invoices', selection: 'multiple', searchable: true, rows: invoices, rowId: (i) => i.id, columns: [ { id: 'customer', header: 'Customer', accessor: (i) => i.customer, filter: 'text' }, { id: 'amount', header: 'Amount', accessor: (i) => i.amount, type: 'currency', currency: 'USD', filter: 'number' }, { id: 'overdue', header: 'Overdue', accessor: (i) => i.overdue, type: 'boolean', filter: 'enum' }, ], }); DataTableColumnChooser.create(columnsButton, table); exportButton.addEventListener('click', () => { downloadCsv(table.toCsv({ rows: 'filtered' }), 'invoices.csv'); }); host.addEventListener('aui-selection-change', (event) => { deleteButton.disabled = (event as CustomEvent<DataTableSelectionChangeDetail>).detail.selected.length === 0; }); deleteButton.addEventListener('click', () => { const doomed = new Set(table.getSelected()); invoices = invoices.filter((i) => !doomed.has(i.id)); table.setRows(invoices); deleteButton.disabled = true; });}More usage
Section titled “More usage”import { DataTable } from '@absoluteui/components/data-table';
const host = document.createElement('div');document.body.append(host);DataTable.create(host, { caption: 'Users', rows: [{ id: 'u1', name: 'Ada', logins: 42 }], rowId: (u) => u.id, multiSort: true, searchable: true, columns: [ { id: 'name', header: 'Name', accessor: (u) => u.name, filter: 'text' }, { id: 'logins', header: 'Logins', accessor: (u) => u.logins, type: 'number', filter: 'number' }, ],});Anatomy
Section titled “Anatomy”<div class="aui-data-table aui-data-table--comfortable aui-data-table--sticky [--virtual] [--fixed] [--pinned]"> (consumer host) <div class="aui-data-table__toolbar" role="search" aria-label="Orders"> (searchable only) <label class="aui-data-table__search"><span class="aui-data-table__search-label">Search</span><input type="search" class="aui-data-table__search-input aui-data-table__control"></label> <div class="aui-data-table__body"> <div class="aui-data-table__region" role="region" tabindex="0" aria-labelledby="{caption id}" [aria-busy="true"]> <table class="aui-data-table__table" [aria-rowcount]> (aria-rowcount in virtual mode) <caption class="aui-data-table__caption" id>Orders</caption> <thead class="aui-data-table__head"> (sticky as a whole) <tr class="aui-data-table__header-row"> <th scope="col" class="aui-data-table__header-cell aui-data-table__select-cell"> (selection) <span class="aui-data-table__box"><input type="checkbox" class="aui-data-table__check aui-data-table__select-all" aria-label="Select all rows">...marks</span> <th scope="col" class="aui-data-table__header-cell aui-data-table__expand-cell"><span class="…visually-hidden">Details</span> (renderDetail) <th scope="col" class="aui-data-table__header-cell [aui-data-table__cell--pinned-start]" data-column="customer" aria-sort="ascending" [aria-label="Customer"]> <button type="button" class="aui-data-table__sort" [aria-describedby="{hint}"]> <span class="aui-data-table__sort-label">Customer</span><span class="…visually-hidden">sort priority 1, ascending</span> <span class="aui-data-table__sort-priority" aria-hidden="true">1</span><svg class="aui-data-table__sort-icon" aria-hidden="true"> <span class="aui-data-table__resizer" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Resize Customer" aria-valuenow> (resizable) <th scope="col" class="aui-data-table__header-cell">Paid</th> (sortable: false) <tr class="aui-data-table__filter-row"> (columns with a filter) <td class="aui-data-table__filter-cell"><input type="search" class="aui-data-table__filter-input aui-data-table__control" aria-label="Filter Customer"> <td class="aui-data-table__filter-cell"><span class="aui-data-table__filter-range"><input type="number" aria-label="Minimum Amount"><input type="number" aria-label="Maximum Amount"> <td class="aui-data-table__filter-cell"><select class="aui-data-table__filter-select" aria-label="Filter Paid"> <tbody class="aui-data-table__rows"> <tr class="aui-data-table__spacer" aria-hidden="true"><td colspan> (virtual mode) <tr class="aui-data-table__row [aui-data-table__row--selected]" [aria-rowindex]> <td class="aui-data-table__cell aui-data-table__select-cell"><span class="aui-data-table__box"><input type="checkbox" aria-label="Select row Acme Corp">...</span></td> <td class="aui-data-table__cell aui-data-table__expand-cell"><button class="aui-data-table__expander" aria-expanded="true" aria-controls="{detail id}" aria-label="Details for Acme Corp"><svg aria-hidden="true"> <th scope="row" class="aui-data-table__cell">Acme Corp</th> <td class="aui-data-table__cell aui-data-table__cell--end">$1,250.50</td> <tr class="aui-data-table__detail-row" id="{detail id}"><td class="aui-data-table__detail" colspan>{renderDetail content} <tr class="aui-data-table__empty-row"><td class="aui-data-table__empty" colspan>No results (no rows) <tr class="aui-data-table__row aui-data-table__skeleton-row" aria-hidden="true">... (loading, no rows) <div class="aui-data-table__status [aui-data-table__status--overlay]" role="status"> (spinner + "Loading") <span id="{hint}" hidden>Hold Shift while activating to sort by more than one column.</span> <div class="aui-data-table__pagination"> (Pagination host; hidden when pageSize is null)The column chooser attaches to a <button> you render (for example next to your own export button): Menu adds aria-haspopup="menu", aria-expanded, and a role="menu" popup with menuitemcheckbox items after the button.
API reference
Section titled “API reference”Factory
Section titled “Factory”DataTable.create<Row>(host: HTMLElement, options: DataTableOptions<Row>): DataTable<Row>; DataTableColumnChooser.create<Row>(trigger: HTMLButtonElement, table: DataTable<Row>, options?: DataTableColumnChooserOptions): DataTableColumnChooser<Row>; downloadCsv(csv: string | Blob, filename: string, doc?: Document): voidGlobal configuration key: dataTable (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
caption |
string |
Table caption; the accessible name of the table and its scroll region. Required. | |
hideCaption |
boolean |
false |
Visually hide the caption (it stays the accessible name). |
columns |
readonly DataTableColumn<Row>[] |
Column definitions: id, header, accessor, and optional type, currency, format, render, sortable, compare, align, width, rowHeader; since 0.3.0 also filter, filterOptions, searchable, hideable, resizable, minWidth, maxWidth, exportable, exportValue; since 0.9.0 also aggregate, renderHeader, editable, editor, editOptions, validate, renderEditor. Required. | |
rows |
readonly Row[] |
All rows (client mode) or the current page (server mode). Required. | |
rowId |
(row: Row) => string |
Stable, unique, non-empty id per row. Duplicates throw DATA_TABLE_ROW_ID. Required. | |
sort |
DataTableSort | readonly DataTableSort[] | null |
null |
Current sort; each column must be sortable and appear once. An array (since 0.3.0) sorts by several columns, first entry first. |
selected |
readonly string[] |
[] |
Selected row ids. |
isRowSelectable |
(row: Row) => boolean |
Rows returning false get a disabled selection input and are skipped by select all and range selection. | |
pageSizes |
readonly number[] |
Render a ‘Rows per page’ select in the composed Pagination. | |
page |
number |
1 |
Current page, 1-based; clamped to the page count. |
mode |
'client' | 'server' |
"client" |
client filters, sorts, and pages rows locally; server renders rows as the current page and only emits events. |
total |
number |
Server mode: total rows across all pages (required when paged). | |
loading |
boolean |
false |
aria-busy on the region, an announced ‘Loading’ status, skeleton rows when empty, otherwise an overlay over existing rows. |
emptyMessage |
string |
"No results (localized)" |
Text shown when there are no rows (also when filters match nothing). |
filters |
Readonly<Record<string, DataTableFilter>> |
{} |
Since 0.3.0. Active column filters by column id; each filter type must match the column’s filter kind (text, number, date, enum). JSON-safe. |
search |
string |
"" |
Since 0.3.0. Global search text: every whitespace-separated term must appear in the displayed text of some visible, searchable column (case-insensitive). |
searchable |
boolean |
false |
Since 0.3.0. Render a labelled search field in a role=search toolbar above the table. |
filterRow |
boolean |
true |
Since 0.3.0. Render the filter row when a visible column has a filter; false keeps filters programmatic. |
hiddenColumns |
readonly string[] |
[] |
Since 0.3.0. Hidden column ids; at least one column stays visible. |
columnOrder |
readonly string[] |
[] |
Since 0.3.0. Display order by column id; unlisted columns follow in definition order. |
columnWidths |
Readonly<Record<string, number>> |
{} |
Since 0.3.0. Border-box column widths in CSS pixels, clamped to each column’s minWidth/maxWidth. Any entry switches the table to a fixed layout. |
pinnedColumns |
{ start?: readonly string[]; end?: readonly string[] } |
{} |
Since 0.3.0. Sticky columns at the inline start and end edges, outermost first (logical edges, so right-to-left layouts mirror). Pinned columns move to their edge; start pins also pin the selection and disclosure columns. |
resizable |
boolean |
false |
Since 0.3.0. Resize handles (focusable separators) on column headers for pointer drag and arrow keys; a column’s resizable overrides it. |
renderDetail |
(row: Row, doc: Document) => Node | string |
Since 0.3.0. Row detail content; enables a disclosure button per row. Called when a row is first expanded; returned Nodes are consumer-owned, strings are text. Not combinable with virtual. | |
isRowExpandable |
(row: Row) => boolean |
Since 0.3.0. Rows returning false get no disclosure button. | |
expanded |
readonly string[] |
[] |
Since 0.3.0. Expanded row ids (pruned like the selection). |
virtual |
boolean |
false |
Since 0.3.0. Windowed rendering of the current page inside the scroll region (single-line rows of uniform height); aria-rowcount and aria-rowindex describe the full set. |
rowHeight |
number |
40 |
Since 0.3.0. Virtual mode: initial row height estimate in CSS pixels (16 to 500), replaced by the measured height. |
groupBy |
readonly string[] |
Group rows by these column ids, nested in order (since 0.9.0). Client mode only; not with virtual. | |
collapsedGroups |
readonly string[] |
Collapsed group keys; groups are expanded by default (since 0.9.0). | |
renderGroupHeader |
(group: DataTableGroup<Row>, doc: Document) => Node |
Custom group header content (since 0.9.0); decorative, released with releaseNode when dropped. | |
showTotals |
boolean |
"true" |
Show the totals row when a column aggregates or totals is set (since 0.9.0). |
totals |
Readonly<Record<string, unknown>> |
Server mode: totals by column id, computed by the server (since 0.9.0). | |
renderEmpty |
(doc: Document) => Node |
Custom empty-state content (since 0.9.0); replaces emptyMessage, released with releaseNode when dropped. | |
icons |
Partial<{ sortAscending: IconRef; sortDescending: IconRef; sortNone: IconRef; checked: IconRef; indeterminate: IconRef; loading: IconRef; expand: IconRef; collapse: IconRef }> |
Icon slot overrides (expand and collapse since 0.3.0). | |
selection |
'none' | 'single' | 'multiple' |
"none" |
Row selection: radios (single) or checkboxes with select all (multiple). Globally configurable. |
pageSize |
number | null |
25 |
Rows per page (1 to 1,000,000); null renders all rows without pagination. Globally configurable. |
stickyHeader |
boolean |
true |
Keep the header rows (and the filter row) visible while the scroll region scrolls. Globally configurable. |
sortCycle |
'tri-state' | 'bi-state' |
"tri-state" |
tri-state: ascending, descending, unsorted. bi-state: ascending, descending. Globally configurable. |
multiSort |
boolean |
false |
Since 0.3.0. Shift+click or Shift+Enter/Space on a sort button adds the column to the sort (or cycles it in place). Globally configurable. |
filterDelay |
number |
250 |
Since 0.3.0. Milliseconds (0 to 5000) between the last keystroke in a text or number filter or the search field and filtering; Enter, blur, and select/date changes apply immediately. Globally configurable. |
locale |
string |
"en-US" |
Messages, number/date formatting, text collation, and case-insensitive matching. Globally configurable. Inherited from common configuration. |
timezone |
string |
"UTC" |
IANA timezone for datetime columns: formatting and date filters. Globally configurable. Inherited from common configuration. |
density |
'compact' | 'comfortable' | 'spacious' |
"comfortable" |
Cell padding. Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
Passed to the composed Pagination (mirrors its chevrons). Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Consumer host; its children are owned by the component. |
table |
HTMLTableElement |
The <table> element. |
sort |
DataTableSort | null |
The primary sort (a copy). |
sorts |
DataTableSort[] |
Since 0.3.0. All sorts by priority (copies). |
filters |
Record<string, DataTableFilter> |
Since 0.3.0. Active filters by column id (a copy). |
search |
string |
Since 0.3.0. Global search text. |
expanded |
string[] |
Since 0.3.0. Expanded row ids (a copy). |
collapsedGroups |
string[] |
Since 0.9.0. Keys of collapsed groups (a copy). |
rowCount |
number |
Since 0.3.0. Rows matching the filters and search across all pages (client mode), or total (server mode). |
page |
number |
Current page (1-based). |
pageSize |
number | null |
Rows per page, or null when paging is off. |
selected |
string[] |
Selected row ids in selection order (a copy). |
options |
Readonly<DataTableOptions<Row>> |
Current instance options. |
trigger | table | menu |
HTMLButtonElement | DataTable<Row> | Menu |
Since 0.3.0. DataTableColumnChooser: its trigger button, the table it controls, and the composed Menu. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
update(options: Partial<DataTableOptions<Row>>): void |
Merge and apply options. Row elements are reused for unchanged rows; selection keeps ids that are still present (client mode). Emits nothing. |
setRows(rows: readonly Row[], total?: number): void |
Replace rows (and the server-mode total). |
getSelected(): string[] |
Selected row ids. |
setSelected(ids: readonly string[]): void |
Replace the selection without events. Unknown (client mode) and unselectable ids are dropped. |
clearSelection(): void |
Clear the selection without events. |
getState(): DataTableState |
Since 0.3.0. JSON-safe snapshot: { version: 1, sort, filters, search, columns: { order, hidden, widths, pinned }, page, pageSize, collapsedGroups (since 0.9.0) }. |
setState(state: DataTableStateInput): void |
Since 0.3.0. Restore a snapshot or any subset of it (omitted parts are unchanged). Validates untrusted input (DATA_TABLE_STATE on malformed structure, state unchanged); unknown column ids and values a column cannot take are dropped. Emits nothing. |
toCsv(options?: DataTableCsvOptions): string |
Since 0.3.0. RFC 4180 CSV of visible/all/named columns and filtered/page/selected/all rows, formatted or raw values, with formula-injection protection. No side effects. |
toCsvBlob(options?: DataTableCsvOptions): Blob |
Since 0.3.0. toCsv() as a text/csv;charset=utf-8 Blob. |
destroy(): void |
Remove rendering, listeners, timers, observers, the composed Pagination, and subscriptions. |
refresh(): void |
Since 0.3.0. Rebuild the chooser items from the table’s columns and visibility (also done on open and after user changes). |
destroy(): void |
Since 0.3.0. Remove the chooser’s listeners and its Menu (the trigger button stays). |
downloadCsv(csv: string | Blob, filename: string, doc?: Document): void |
Since 0.3.0. Save CSV through a temporary object URL and an <a download> click. The only download side effect; nothing calls it implicitly. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-sort-change |
{ sort: DataTableSort | null; sorts: DataTableSort[] } |
Yes | No | The user activated a sort button (sorts since 0.3.0). Informative in client mode; in server mode load sorted rows. The page returns to 1. |
aui-selection-change |
{ selected: string[] } |
Yes | No | The user changed the selection (row input, select all, or Shift+click range). |
aui-page-change |
{ page: number; pageSize: number } |
Yes | No | The user changed the page or page size. Informative in client mode; in server mode load that page. |
aui-filter-change |
{ filters: Record<string, DataTableFilter>; search: string } |
Yes | No | Since 0.3.0. The user changed a column filter or the global search. The page returns to 1. In server mode load matching rows (read table.getState() for the full query). |
aui-columns-change |
{ column: string; reason: 'visibility' | 'resize'; columns: DataTableColumnsState } |
Yes | No | Since 0.3.0. The user showed or hid a column (column chooser) or resized one (on release, or per resize key). |
aui-expand-change |
{ id: string; expanded: boolean; expandedIds: string[] } |
Yes | No | Since 0.3.0. The user expanded or collapsed a row. |
aui-group-toggle |
{ key: string; collapsed: boolean; collapsedGroups: string[] } |
Yes | No | The user expanded or collapsed a group (since 0.9.0). |
aui-cell-edit |
{ rowId: string; row: Row; column: string; value: unknown; previous: unknown } |
Yes | No | The user committed an inline edit (since 0.9.0); update rows with value. |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
column render() |
Custom cell content per column: a string (rendered as text) or a consumer-owned Node. |
renderDetail() |
Since 0.3.0. Row detail content under an expanded row: a consumer-owned Node or a string (text). Kept while the row stays expanded and its object is unchanged. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-data-table |
Host. |
aui-data-table--compact | --comfortable | --spacious |
Density. |
aui-data-table--sticky |
Sticky header enabled. |
aui-data-table--loading |
Loading state. |
aui-data-table--virtual |
Since 0.3.0. Virtual mode (the region gets max-block-size: 70vh; override it on the region). |
aui-data-table--fixed |
Since 0.3.0. Fixed table layout with pixel column widths (after a resize or with columnWidths). |
aui-data-table--pinned |
Since 0.3.0. At least one column is pinned. |
aui-data-table--resizing |
Since 0.3.0. A pointer resize is in progress. |
aui-data-table__toolbar | __search | __search-label | __search-input |
Since 0.3.0. Search toolbar (role=search) and its labelled field. |
aui-data-table__control |
Since 0.3.0. Native filter and search fields. |
aui-data-table__body |
Positioning wrapper for the region and the loading status. |
aui-data-table__region |
Focusable, labelled scroll region. |
aui-data-table__table |
The <table>. |
aui-data-table__caption |
The <caption>. |
aui-data-table__head | __header-row |
The <thead> (sticky as a whole) and its header row. |
aui-data-table__header-cell |
Column header <th scope=col>. |
aui-data-table__header-cell--sorted |
A sorted column header (every sort column). |
aui-data-table__sort | __sort-label | __sort-icon | __sort-priority |
Sort button and its parts (priority number since 0.3.0, shown while sorting by several columns). |
aui-data-table__resizer |
Since 0.3.0. Column resize handle (role=separator). |
aui-data-table__filter-row | __filter-cell | __filter-input | __filter-range | __filter-select |
Since 0.3.0. Filter row and its controls. |
aui-data-table__cell--pinned | --pinned-start | --pinned-end | --pinned-edge |
Since 0.3.0. Sticky cells of pinned columns; the edge class marks the innermost pinned column of each side. |
aui-data-table__expand-cell | __expander | __expander-icon |
Since 0.3.0. Disclosure column and its button. |
aui-data-table__detail-row | __detail |
Since 0.3.0. Detail row of an expanded row and its full-width cell. |
aui-data-table__spacer |
Since 0.3.0. Virtual mode spacer rows (aria-hidden). |
aui-data-table__row |
Body row. |
aui-data-table__row--selected |
Selected row. |
aui-data-table__cell |
Body cell (td, or th scope=row for the row header). |
aui-data-table__cell--end | --center |
Cell and header alignment. |
aui-data-table__select-cell |
Selection column cell. |
aui-data-table__box | __check | __check--checkbox | __check--radio | __mark |
Native selection input and its mark. |
aui-data-table__select-all |
Select-all checkbox. |
aui-data-table__empty-row | __empty |
Empty state row and cell. |
aui-data-table__skeleton-row | __skeleton |
Decorative loading placeholders. |
aui-data-table__status | __status--overlay | __spinner |
Loading status region and overlay. |
aui-data-table__pagination |
Host of the composed Pagination. |
aui-data-table__visually-hidden |
Visually hidden text (caption, labels). |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
sortAscending |
arrow-up |
sortDescending |
arrow-down |
sortNone |
arrow-up-down |
checked |
check |
indeterminate |
minus |
loading |
spinner |
expand |
chevron-right |
collapse |
chevron-down |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-border-width | --aui-color-active | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-on-primary | --aui-color-primary | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-control-height-sm | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --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-lg | --aui-icon-size-md | --aui-icon-size-sm | --aui-line-height-normal | --aui-radius-md | --aui-radius-pill | --aui-radius-sm | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-4 | --aui-space-8 | --aui-touch-target-min
TypeScript definitions
Section titled “TypeScript definitions”type DataTableColumnType = 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'boolean';interface DataTableSort { column: string; direction: 'ascending' | 'descending' }type DataTableFilter = | { type: 'text'; value: string; operator?: 'contains' | 'equals' | 'startsWith' } | { type: 'number'; min?: number | null; max?: number | null } | { type: 'date'; from?: string | null; to?: string | null } | { type: 'enum'; values: readonly string[] };interface DataTableColumn<Row> { id: string; header: string; accessor: (row: Row) => unknown; type?: DataTableColumnType; currency?: string; format?: (value: unknown, row: Row) => string; render?: (row: Row, doc: Document) => Node | string; sortable?: boolean; compare?: (a: Row, b: Row) => number; align?: 'start' | 'end' | 'center'; width?: string; rowHeader?: boolean; filter?: 'text' | 'number' | 'date' | 'enum'; filterOptions?: readonly { value: string; label: string }[]; searchable?: boolean; hideable?: boolean; resizable?: boolean; minWidth?: number; maxWidth?: number; exportable?: boolean; exportValue?: (row: Row) => string | number | boolean | null;}interface DataTableState { version: 1; sort: DataTableSort[]; filters: Record<string, DataTableFilter>; search: string; columns: { order: string[]; hidden: string[]; widths: Record<string, number>; pinned: { start: string[]; end: string[] } }; page: number; pageSize: number | null;}interface DataTableSortChangeDetail { sort: DataTableSort | null; sorts: DataTableSort[] }interface DataTableSelectionChangeDetail { selected: string[] }interface DataTablePageChangeDetail { page: number; pageSize: number }interface DataTableFilterChangeDetail { filters: Record<string, DataTableFilter>; search: string }interface DataTableColumnsChangeDetail { column: string; reason: 'visibility' | 'resize'; columns: DataTableState['columns'] }interface DataTableExpandChangeDetail { id: string; expanded: boolean; expandedIds: string[] }class DataTable<Row> { static create<Row>(host: HTMLElement, options: DataTableOptions<Row>): DataTable<Row>; getState(): DataTableState; setState(state: DataTableStateInput): void; toCsv(options?: DataTableCsvOptions): string; toCsvBlob(options?: DataTableCsvOptions): Blob;}class DataTableColumnChooser<Row> { static create<Row>(trigger: HTMLButtonElement, table: DataTable<Row>, options?: DataTableColumnChooserOptions): DataTableColumnChooser<Row>;}function downloadCsv(csv: string | Blob, filename: string, doc?: Document): void;Accessibility
Section titled “Accessibility”Pattern: WAI-ARIA APG Table pattern using a native HTML <table> (not the Grid pattern) with sortable column headers (APG ‘Sortable Table’ example): header buttons plus aria-sort on the primary sorted column. Since 0.3.0: column resize handles follow the APG Window Splitter pattern (focusable separator with value); row expansion follows the APG Disclosure pattern; the column chooser is an APG Menu Button with menuitemcheckbox items; virtual rows use aria-rowcount/aria-rowindex.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Any | Moves through the search field, the scroll region, sort buttons, resize handles, filter controls, select all, row selection inputs, disclosure buttons, interactive cell and detail content, and pagination controls (native order). |
| Enter / Space | Sort button focused | Sorts by this column only (ascending, descending, then unsorted in tri-state). |
| Shift+Enter / Shift+Space / Shift+click | Sort button, multiSort enabled | Adds the column to the sort as the lowest priority, or cycles it in place (removed after descending in tri-state). |
| ArrowRight / ArrowLeft | Resize handle focused | Widens / narrows the column by 10 px in ltr (mirrored in rtl); with Shift by 50 px. Emits aui-columns-change. |
| Home / End | Resize handle focused | Sets the column to its minimum / maximum width. |
| Typing | Text or number filter, search field | Filters after filterDelay (default 250 ms); Enter applies immediately. |
| Enter / Space | Disclosure button focused | Expands or collapses the row’s detail row; focus stays on the button. |
| Space | Checkbox focused | Toggles the row, or all selectable rows on the page (native). |
| Arrow keys | Row radio focused (single selection) | Selects the previous/next row (native radio group). |
| Arrow keys | Scroll region focused | Scrolls the table (native); in virtual mode rows render as they scroll into view. |
| Shift+click | Row checkbox | Sets every selectable row between the last clicked row and this one (current page) to this row’s new state. |
| Enter / Space / ArrowDown | Column chooser button | Opens the menu (APG Menu Button); Space on an item toggles the column; Escape closes and returns focus. |
| Enter / Space | Editable cell button | Opens the cell editor (checkbox columns toggle the value in one step); focus moves into the editor. |
| Enter / Tab (leaving the cell) | Cell editor | Commits the value; a validation message keeps the editor open with aria-invalid and the message. |
| Escape | Cell editor | Cancels the edit and returns focus to the cell button. |
| Enter / Space | Group header toggle | Collapses or expands the group (aria-expanded). |
| Element | Role |
|---|---|
| <div class=aui-data-table__region> | region, named by the caption; tabindex=0 so keyboard users can scroll |
| <table> | table (native) |
| <caption> | table caption (native accessible name) |
| <th scope=col> | columnheader (native) |
| <th scope=row> | rowheader (native) |
| <button> in a header | button (native) that changes the sort |
| <input type=checkbox | radio> | checkbox / radio (native) for row selection |
| <div class=aui-data-table__status> | status (announces ‘Loading’) |
| composed Pagination | navigation landmark named ‘{caption}, pagination’ |
| <div class=aui-data-table__toolbar> | search landmark named by the caption (searchable only) |
| <input type=search> in the toolbar | searchbox (native), labelled by a visible ‘Search’ label |
| filter row <input>/<select> | searchbox, spinbutton, date input, or combobox (native), each with an aria-label naming the column |
| <span class=aui-data-table__resizer> | separator (focusable, aria-orientation=vertical) |
| <button class=aui-data-table__expander> | button (native) with aria-expanded (disclosure) |
| <tr class=aui-data-table__detail-row> | row with one full-width cell holding the detail content |
| DataTableColumnChooser | Menu button (aria-haspopup=menu, aria-expanded) with role=menu and menuitemcheckbox items |
| Editable cell | button inside the cell (td/th), named by the cell value plus “, edit {column}” (visually hidden); checkbox columns use aria-pressed |
| Group header row | row with a cell spanning all columns; the toggle is a button with aria-expanded, named “{column}: {value} ({count})” |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-labelledby |
scroll region | The caption id |
aria-sort |
the primary sorted column header only | ascending | descending (absent on other columns, including secondary sort columns) |
button text (visually hidden) |
sort buttons of sort columns while sorting by several columns | ‘sort priority {n}, ascending | descending’ appended to the header text; the visible number is aria-hidden |
aria-label |
column headers (resizable only) | The header text, so the resize handle’s name is not part of the column header name |
aria-describedby |
sort buttons (multiSort only) | A hidden hint: ‘Hold Shift while activating to sort by more than one column.’ |
aria-label |
select-all checkbox | Localized ‘Select all rows’; indeterminate property exposes mixed state natively |
aria-label |
row checkbox / radio | Localized ‘Select row {row header text}’ (row id when there is no row header) |
aria-label |
filter controls | ‘Filter {column}’, ‘Minimum/Maximum {column}’, ‘{column} from/to’ |
aria-label |
resize handle | ‘Resize {column}’ |
aria-valuenow / aria-valuemin / aria-valuemax / aria-valuetext |
resize handle | Width in CSS pixels, the column’s minWidth/maxWidth, and ‘{width} pixels’ (measured when the handle receives focus) |
aria-expanded / aria-controls |
disclosure button | true | false; aria-controls points at the detail row while it is shown |
aria-label |
disclosure button | Localized ‘Details for {row header text}’ |
aria-rowcount / aria-rowindex |
table / header rows and rendered body rows (virtual mode only) | Header rows plus all rows of the filtered set; 1-based positions including header rows and the page offset |
aria-busy |
scroll region | true while loading |
aria-hidden |
skeleton rows, virtual spacer rows, sort, mark, and disclosure icons, sort priority number | true (decorative) |
disabled |
selection inputs of unselectable rows | Native disabled |
Header buttons persist across sorts, so focus stays on the activated button. Row elements are reused by row id and unchanged rows are not moved, so focus inside them survives selection changes, expansion, and data refreshes. Filter fields are not overwritten while focused. In virtual mode the row that holds focus stays rendered while it scrolls out of the window, so focus is never lost to the document. Pagination keeps focus on its own controls after a page change.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- Sort changes are announced politely: ‘Sorted by {column}, ascending’ / ‘descending’, ‘Sorted by A, ascending, then by B, descending’ for several columns, or ‘Sorting cleared’. Programmatic sorts are silent.
- Filter and search changes are announced politely in client mode: ‘Results: {count}’. In server mode the consumer’s new rows update the pagination summary.
- Selected rows use the checked input plus a row class (aui-data-table__row–selected); aria-selected is not used because this is a plain table, not a grid.
- The loading status lives outside the busy region so its ‘Loading’ text is announced.
- Select all applies to the current page; its checked/indeterminate state reflects only that page.
- Resize handles are 1rem wide, centered on the column edge; the pointer path needs a drag (WCAG 2.5.7): keyboard resizing and columnWidths/setState are the alternatives.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
- The scroll region is always a tab stop, even when the table does not overflow.
- Each resize handle is a tab stop, which doubles header tab stops with resizable: true.
- Virtual mode exposes only rendered rows to assistive technology; aria-rowcount and aria-rowindex convey the full size and positions.
Behavior
Section titled “Behavior”States
Section titled “States”default, row hover, row selected, row not selectable (disabled input), sorted ascending/descending, unsorted, sorted by several columns (priority numbers), filtered, focus-visible, loading with skeleton rows, loading with an overlay over existing rows, empty (no rows or no matches), density (compact/comfortable/spacious), sticky header, column hidden, column resized (fixed layout), resizing, columns pinned, row expanded, virtual.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| unsorted | sort button | ascending; page 1; announcement; aui-sort-change |
| ascending | sort button | descending |
| descending | sort button | unsorted (tri-state) or ascending (bi-state) |
| sorted by A | Shift + sort button B (multiSort) |
sorted by A, then B ascending; priorities shown |
| sorted by A, B | Shift + sort button B | B cycles in place; after descending (tri-state) B is removed |
| sorted by several | sort button without Shift | only that column, continuing its own cycle |
| any | filter field input (after filterDelay), Enter, select or date change |
filtered; page 1; “Results: n” announced (client); aui-filter-change |
| any | search field input | searched; page 1; aui-filter-change |
| column visible | chooser item | hidden; aui-columns-change (visibility); the last visible column cannot be hidden |
| auto layout | first resize (pointer or key) | fixed layout: every header gets its measured pixel width |
| width w | ArrowRight / ArrowLeft / Home / End on a handle, or drag | new width within min/max; aui-columns-change (resize) per key or on release |
| collapsed row | disclosure button | expanded: detail row after the row; aui-expand-change |
| expanded row | disclosure button | collapsed: detail row removed |
| any page | Pagination control | new page (and size); aui-page-change |
| unselected row | row checkbox / radio | selected; aui-selection-change |
| anchor row set | Shift+click another row checkbox | every selectable row between them takes the clicked row’s new state |
| some or none selected on page | select all | every selectable row on the page selected |
| all selected on page | select all | every row on the page deselected |
| rows present | update({ loading: true }) |
overlay spinner; rows kept |
| no rows | update({ loading: true }) |
skeleton rows |
| any | setRows(rows) |
re-render; rows whose object is unchanged keep their element; selection pruned to present ids (client mode) |
| any | setState(state) |
the given parts restored; no events |
Accessibility notes
Section titled “Accessibility notes”- User sort changes are announced politely: “Sorted by Customer, ascending”, “Sorted by Dept, ascending, then by Salary, descending”, or “Sorting cleared”.
- Client-side filter and search changes are announced politely: “Results: 12”.
- Sort buttons persist across sorts, so focus stays put. Unchanged rows are neither rebuilt nor moved, so focus inside them survives selection changes, expansion, and refreshes. Filter fields are not overwritten while they have focus.
- Virtual mode keeps the row that holds focus rendered while it scrolls out of the window, so focus never falls back to the document.
- Select all applies to the current page (every row of it in virtual mode). Its state reflects only that page; selections on other pages are kept. Filters do not clear the selection.
- Selected rows are shown by fill and the checked mark, not color alone. Radios show a filled dot.
- Pagination announces the new range (“Showing 11-20 of 57”) after a page change.
- Resizing by pointer needs a drag. Its alternatives are keyboard resizing on the focused handle and
columnWidths/setState(WCAG 2.5.7; see Known limitations). - Each resize handle is a tab stop, so
resizable: truedoubles the header’s tab stops. - Manual screen-reader verification is pending (see Known limitations).
Responsive behavior
Section titled “Responsive behavior”The table sits in a scroll region (overflow: auto) that keyboard users can focus and scroll, so wide tables never force page-level horizontal scrolling (WCAG 1.4.10). With stickyHeader (default), the whole head (header and filter rows) is position: sticky inside the region; give the region a max-block-size (for example .orders .aui-data-table__region { max-block-size: 60vh; }) for it to stick on long tables. Pinned columns stay in view while the region scrolls horizontally, which keeps the row identity visible on narrow screens; pin few columns there, because they do not scroll. The search toolbar wraps; filter fields keep a minimum width and scroll with the table. Virtual mode gives the region max-block-size: 70vh. The composed Pagination switches to “Page 3 of 10” in narrow containers.
Right-to-left
Section titled “Right-to-left”Layout uses logical properties and follows the document direction; end-aligned numeric columns align to the left edge in rtl. Pinned columns use inset-inline-start/inset-inline-end, so start pins sit on the right in rtl and the edge boundary follows. Resize handles sit on each column’s inline-end edge; in rtl ArrowLeft widens a column and a drag to the left widens it (the direction is read from the computed style, so dir on any ancestor works). The collapsed disclosure chevron is mirrored. The direction option is passed to Pagination, which mirrors its chevrons. Sort arrows point up and down and are not mirrored.
Theming
Section titled “Theming”Colors come from role tokens only. Headers and the filter row use color-surface-sunken, hovered rows color-hover, and selected rows color-selected with color-selected-text. Pinned cells get an opaque background that follows the row state (surface, hover, selected) and a color-border-strong boundary on the innermost pinned column. Filter and search fields use color-surface with a color-border-strong boundary; native date and number fields follow the theme’s color-scheme. Detail rows use color-surface-sunken. Selection inputs match Checkbox. In forced-colors mode selected rows get a Highlight outline, inputs use Canvas, CanvasText, and Highlight, and resize handles and pinned boundaries use CanvasText.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { dataTable: { selection: 'multiple', pageSize: 50, sortCycle: 'bi-state', multiSort: true, filterDelay: 300 } } });Changes apply to existing instances immediately (rows are rebuilt).
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic. A page size chosen by the user in the rows-per-page select overrides configuration until update({ pageSize }) is called. View state (sort, filters, search, column order/visibility/widths/pinning, page, page size) can be set with individual options, with update(), or all at once with setState().
Timezones
Section titled “Timezones”datetime columns expect an Instant (or ZonedDateTime) and format it with formatInstant in the resolved timezone (instance, then component, then common configuration; UTC by default). The host machine’s zone is never used, and a ZonedDateTime is also shown in the configured zone so a column never mixes zones. For example, instant(Date.UTC(2024, 0, 15, 15, 30)) shows “Jan 15, 2024, 3:30 PM” in UTC and “Jan 15, 2024, 10:30 AM” in America/New_York. date columns expect a PlainDate and never shift. Sorting uses epoch milliseconds (datetime) and calendar fields (date).
Date filters (since 0.3.0) on datetime columns compare the calendar date in the configured timezone: from and to days are converted to the start instants of those days in the zone (compatible disambiguation, so a day that starts in a DST gap starts at the first valid time), and a value matches when it falls in [start of from, start of the day after to). So 2024-01-15T03:00Z matches the day 2024-01-14 in America/New_York but 2024-01-15 in UTC. On date columns the filter compares calendar fields only. Raw CSV exports write YYYY-MM-DD for dates and ISO 8601 UTC (Z) for instants; formatted exports use the configured zone.
icons.sortAscending (default arrow-up), icons.sortDescending (arrow-down), icons.sortNone (arrow-up-down), icons.checked (check), icons.indeterminate (minus), icons.loading (spinner), and since 0.3.0 icons.expand (chevron-right, mirrored in rtl) and icons.collapse (chevron-down).
Security
Section titled “Security”All cell text uses textContent. format() results and render()/renderDetail() strings are text, never HTML. Nodes returned by render() and renderDetail() are consumer-owned and appended as-is, so their safety is the consumer’s responsibility. Column widths must match ^\d+(\.\d+)?(px|rem|em|%|ch)$ (the width option) or be finite pixel numbers (columnWidths); widths, the fixed layout, and pinned offsets (CSS custom properties) are written with style.setProperty. CSP-safe, including Trusted Types (no HTML sinks).
CSV export (since 0.3.0) follows RFC 4180: fields that contain the delimiter, a double quote, CR, or LF are quoted and quotes are doubled. Formula injection (OWASP “CSV Injection”): a cell whose text starts with =, +, -, @, a tab, or a carriage return is prefixed with a single quote and quoted, so spreadsheet applications show it as text. This applies to headers, format() output, text values, and strings from exportValue. Numbers the table formats itself (number and currency columns without format), raw numbers and booleans, and numbers or booleans returned by exportValue are trusted: they cannot carry a formula, so -5 stays a number. escapeFormulas: false disables the protection; use it only for consumers that are not spreadsheets. toCsv() and toCsvBlob() have no side effects; downloadCsv() is the only function that starts a download, through a temporary object URL (revoked after one second) and an <a download> element that is removed immediately.
setState() treats its input as untrusted (for example parsed from storage or a URL): malformed structure throws DATA_TABLE_STATE and leaves the table unchanged.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. Rows are held by reference and never mutated. In server mode the component only emits events; you fetch data. getState() returns a JSON-safe snapshot; storing it (for example in localStorage) is your choice, and it contains filter and search text the user typed. CSV exports contain whatever the chosen columns show; exclude sensitive columns with exportable: false.
Performance
Section titled “Performance”- Rows are built into a
DocumentFragmenton first render. Afterwards row elements are reused by row id while the row object and rendering inputs (columns, column layout, locale, timezone, selection mode, configuration) are unchanged; unchanged rows are not moved. - Only the current page (or, in virtual mode, the rows in view plus 6 rows of overscan on each side, at most 300) is in the DOM.
- Sorted order is cached until rows, sorts, columns, or locale change, so paging and filtering do not re-sort. Filtered rows are cached until the sorted rows, filters, or search change.
- Sort keys are computed once per row and sort column. Text keys are ranked once with one
Intl.Collator(numeric: true) per locale, so multi-column comparisons are numeric. - Text filters and search use a per-column cache of the lower-cased displayed text (built on first use, invalidated when rows, columns, locale, timezone, or configuration change).
- Delegated listeners on the head, the body, the toolbar, and the region; one Pagination listener; one configuration subscription; one shared ResizeObserver entry for the region and one per pinned header cell. Filter typing is debounced (
filterDelay). - Resizing and pinning read layout (header cell widths) only when those features are in use.
- Budgets (headless Chromium, e2e performance test; sort, filter, and CSV figures are the median of 5): rendering 1,000 rows x 8 columns with
pageSize: nullunder 150 ms; sorting 10,000 rows withpageSize: 25under 100 ms for text columns and under 50 ms for numeric columns; multi-column sort (text then number) of 10,000 rows under 120 ms; filtering 10,000 rows under 50 ms; creating a virtual table of 100,000 rows under 200 ms and re-rendering its window after a scroll under 16 ms (median of 10 jumps);toCsv()of 10,000 rows x 8 columns under 150 ms. For larger client-side data sets, sort and filter on the server (mode: 'server').
Supported data scale
Section titled “Supported data scale”Client mode is tested with 10,000 rows paged (filtering, multi-sort, CSV), 1,000 rows x 8 columns rendered at once, and 100,000 rows with virtual: true. Virtual scrolling is bounded by the browser’s maximum element height (about 33,000,000 px in Chromium, roughly 800,000 rows of 40 px). For larger sets, use server mode. Page counts follow Pagination (any integer).
Edge cases
Section titled “Edge cases”null,undefined,NaN, and values that do not match the column type render as empty cells, sort last in both directions, and never match number or date ranges.- Equal sort keys keep their original order (stable sort), also across several sort columns.
- Text sorting is locale-aware with numeric collation: “Item 2” sorts before “Item 10”.
- A custom
comparereceives rows and is negated for descending; nulls-last handling is then up to the comparator. - A sort on a column that is removed or made unsortable by
update({ columns })is dropped; so are filters the new columns cannot take, and hidden, width, order, and pin entries of removed columns. - A plain (non-Shift) sort activation while sorting by several columns sorts by that column only, continuing its own cycle (a descending secondary column becomes unsorted in tri-state).
- Filters on hidden columns stay active; the search only covers visible, searchable columns.
- A programmatic enum filter with several values shows “n selected” in the filter row; choosing an option there replaces it.
- Shrinking
rowsor filtering clamps the page.total: 0or no matching rows shows the empty message and hides the pagination. update({ selection: 'none' })clears the selection; switching to'single'keeps the most recent id.- Server mode keeps selected and expanded ids from other pages; call
clearSelection()when the data set changes. - Shift+click with no anchor on the current page (for example after paging) selects just that row and sets the anchor.
- The first resize switches the table to a fixed layout with every header’s measured width; after a column is shown or hidden the layout is measured again (resized widths are kept). Widths are border-box pixels.
pinnedColumnsentries for hidden columns are ignored while hidden. Start pins also pin the selection and disclosure columns, which sit at the start edge.hiddenColumnsmust leave one column visible; asetStatesnapshot that would hide every column keeps all columns visible.- A detail row renders its content once per row object;
setRowswith new row objects renders it again for expanded rows. - Virtual mode with a region that has no bounded height renders at most 300 rows; bound the region.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
DATA_TABLE_OPTIONS |
configuration | Empty caption; empty columns; duplicate or empty column id; empty header; missing accessor; unknown type or align; currency column without an ISO 4217 code; invalid width; more than one rowHeader: true; sort on an unknown or unsortable column, or a column twice; invalid selected, pageSizes, page, mode, total, loading, or emptyMessage; a pageSize not in pageSizes; more than one selected id with single selection; server mode without total while paged. Since 0.3.0: a filter kind the column type cannot take, filterOptions without an enum filter, invalid minWidth/maxWidth/booleans on a column; filters for unknown columns or of the wrong type; invalid search, hiddenColumns (unknown ids, or all columns), columnOrder, columnWidths, pinnedColumns (unknown ids, or a column on both edges), renderDetail, isRowExpandable, expanded, rowHeight; virtual together with renderDetail; a column chooser without a DataTable. |
DATA_TABLE_ROW_ID |
configuration | rowId returned an empty or non-string value, or two rows share an id. |
DATA_TABLE_STATE |
configuration | Since 0.3.0. setState input that is not a version: 1 state object, or whose parts have the wrong structure. The table is unchanged. |
DATA_TABLE_CSV |
configuration | Since 0.3.0. Invalid toCsv options (unknown column id, delimiter, rows, values, line ending) or an empty downloadCsv file name. |
MENU_TRIGGER |
configuration | The column chooser trigger is not a <button> attached to the document. |
CFG_INVALID_VALUE |
configuration | Invalid selection, pageSize, stickyHeader, sortCycle, multiSort, filterDelay, locale, timezone, density, or direction. |
A failed construction (including an exception thrown by a consumer render/format) leaves no rendering, listeners, or subscriptions behind. A failed update or setState leaves the previous state in place.
Known limitations
Section titled “Known limitations”- Plain table semantics: no cell-by-cell arrow-key navigation or cell selection. Inline editing opens one editor at a time, and editable cells are tab stops.
- No column reorder by drag, nested grids, or infinite scrolling. Column order is programmatic (
columnOrder,setState). Grouping and aggregation run in client mode only and not withvirtual. - Pointer resizing needs a drag; the alternatives are the keyboard handle and
columnWidths/setState(WCAG 2.5.7 is met through those alternatives; flagged for accessibility review). - Enum filters in the filter row choose one value; several values are programmatic.
- Text filters and search are case-insensitive substring matches on displayed text; accents and punctuation are significant.
- Filters and search are not applied in server mode; the consumer applies them.
- Virtual rows are single-line (
white-space: nowrap) and must share one height; virtual mode cannot be combined with row expansion. - Select all covers the current page only; there is no “select all N results”. Filters do not change the selection.
- Range selection works within the current page.
- The sticky header needs a
max-block-sizeon the region to stick on long tables. - The scroll region is always a tab stop; each resize handle and each editable cell is a tab stop.
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- ARIA grid semantics and arrow-key cell navigation.
- Infinite scrolling and lazily loaded virtual windows.
- HTML strings in cells or detail rows.
virtual: truewithrenderDetail.