Skip to content
ShineUI
Install

DataTable

The ledger table.

<sui-data-table>DataStable

About

The ledger table: a real table element that sorts, filters, groups, selects and edits, with frozen columns, a sticky header, opt-in virtual rows and a stacked layout on narrow screens. Focus lands on the row, not the cell. Reach for it for records a person reads, and for sui-data-grid when the data is too large to read.

Usage

<sui-data-table footer-meta="6 of 143 invoices"></sui-data-table> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-data-table"); el.columns = [ { key: "ref", header: "Invoice #", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, { key: "cust", header: "Customer", sortable: true, filter: true, flex: 2, minWidth: 190 }, { key: "due", header: "Due date", type: "date", sortable: true, filter: true, nowrap: true, width: 140, format: (v) => { /* ... */ } }, { key: "amount", header: "Amount", money: true, sortable: true, filter: true, width: 140, format: (v) => { /* ... */ } }, { key: "st", header: "Status", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, ]; /* SuiDataTableColumn[] */ el.rows = [ { ref: "INV-1043", cust: "Silver Hills Church", due: "2026-09-07", amount: 6420, st: "Open", memo: "Quarterly bookkeeping and payroll filing." }, { ref: "INV-1042", cust: "Riverbend Dental Group", due: "2026-08-02", amount: 12480, st: "Overdue", memo: "Year-end close and two amended returns." }, { ref: "INV-1041", cust: "Clark County Schools", due: "2026-08-27", amount: 18940.45, st: "Open", memo: "Grant reconciliation, July and August." }, { ref: "INV-1039", cust: "Meridian Fitness", due: "2026-08-11", amount: 3890, st: "Paid", memo: "Monthly bookkeeping." }, { ref: "INV-1036", cust: "Northgate Auto Body", due: "2026-07-30", amount: 7215.5, st: "Paid", memo: "Sales tax filing, second quarter." }, { ref: "INV-1034", cust: "Harbour Point Marina", due: "2026-09-19", amount: 2450, st: "Draft", memo: "Slip revenue review." }, ]; /* SuiGridRow[] */ el.addEventListener("suiSelectionChange", (e) => console.log(e.detail)); </script>
import { SuiDataTable } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const columns = [ { key: "ref", header: "Invoice #", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, { key: "cust", header: "Customer", sortable: true, filter: true, flex: 2, minWidth: 190 }, { key: "due", header: "Due date", type: "date", sortable: true, filter: true, nowrap: true, width: 140, format: (v) => { /* ... */ } }, { key: "amount", header: "Amount", money: true, sortable: true, filter: true, width: 140, format: (v) => { /* ... */ } }, { key: "st", header: "Status", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, ]; /* SuiDataTableColumn[] */ const rows = [ { ref: "INV-1043", cust: "Silver Hills Church", due: "2026-09-07", amount: 6420, st: "Open", memo: "Quarterly bookkeeping and payroll filing." }, { ref: "INV-1042", cust: "Riverbend Dental Group", due: "2026-08-02", amount: 12480, st: "Overdue", memo: "Year-end close and two amended returns." }, { ref: "INV-1041", cust: "Clark County Schools", due: "2026-08-27", amount: 18940.45, st: "Open", memo: "Grant reconciliation, July and August." }, { ref: "INV-1039", cust: "Meridian Fitness", due: "2026-08-11", amount: 3890, st: "Paid", memo: "Monthly bookkeeping." }, { ref: "INV-1036", cust: "Northgate Auto Body", due: "2026-07-30", amount: 7215.5, st: "Paid", memo: "Sales tax filing, second quarter." }, { ref: "INV-1034", cust: "Harbour Point Marina", due: "2026-09-19", amount: 2450, st: "Draft", memo: "Slip revenue review." }, ]; /* SuiGridRow[] */ return <SuiDataTable footerMeta="6 of 143 invoices" columns={columns} rows={rows} onSuiSelectionChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiDataTable } from "@shineui/vue"; import "@shineui/core/shineui.css"; const columns = [ { key: "ref", header: "Invoice #", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, { key: "cust", header: "Customer", sortable: true, filter: true, flex: 2, minWidth: 190 }, { key: "due", header: "Due date", type: "date", sortable: true, filter: true, nowrap: true, width: 140, format: (v) => { /* ... */ } }, { key: "amount", header: "Amount", money: true, sortable: true, filter: true, width: 140, format: (v) => { /* ... */ } }, { key: "st", header: "Status", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, ]; // SuiDataTableColumn[] const rows = [ { ref: "INV-1043", cust: "Silver Hills Church", due: "2026-09-07", amount: 6420, st: "Open", memo: "Quarterly bookkeeping and payroll filing." }, { ref: "INV-1042", cust: "Riverbend Dental Group", due: "2026-08-02", amount: 12480, st: "Overdue", memo: "Year-end close and two amended returns." }, { ref: "INV-1041", cust: "Clark County Schools", due: "2026-08-27", amount: 18940.45, st: "Open", memo: "Grant reconciliation, July and August." }, { ref: "INV-1039", cust: "Meridian Fitness", due: "2026-08-11", amount: 3890, st: "Paid", memo: "Monthly bookkeeping." }, { ref: "INV-1036", cust: "Northgate Auto Body", due: "2026-07-30", amount: 7215.5, st: "Paid", memo: "Sales tax filing, second quarter." }, { ref: "INV-1034", cust: "Harbour Point Marina", due: "2026-09-19", amount: 2450, st: "Draft", memo: "Slip revenue review." }, ]; // SuiGridRow[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiDataTable footer-meta="6 of 143 invoices" :columns="columns" :rows="rows" @sui-selection-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiDataTable } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiDataTable], template: `<sui-data-table footer-meta="6 of 143 invoices" [(selection)]="selection" [columns]="columns" [rows]="rows"></sui-data-table>`, }) export class Example { selection?: SuiGridExcludeSelection | SuiGridIncludeSelection; columns = [ { key: "ref", header: "Invoice #", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, { key: "cust", header: "Customer", sortable: true, filter: true, flex: 2, minWidth: 190 }, { key: "due", header: "Due date", type: "date", sortable: true, filter: true, nowrap: true, width: 140, format: (v) => { /* ... */ } }, { key: "amount", header: "Amount", money: true, sortable: true, filter: true, width: 140, format: (v) => { /* ... */ } }, { key: "st", header: "Status", sortable: true, filter: true, width: 130, render: (r) => { /* ... */ } }, ]; // SuiDataTableColumn[] rows = [ { ref: "INV-1043", cust: "Silver Hills Church", due: "2026-09-07", amount: 6420, st: "Open", memo: "Quarterly bookkeeping and payroll filing." }, { ref: "INV-1042", cust: "Riverbend Dental Group", due: "2026-08-02", amount: 12480, st: "Overdue", memo: "Year-end close and two amended returns." }, { ref: "INV-1041", cust: "Clark County Schools", due: "2026-08-27", amount: 18940.45, st: "Open", memo: "Grant reconciliation, July and August." }, { ref: "INV-1039", cust: "Meridian Fitness", due: "2026-08-11", amount: 3890, st: "Paid", memo: "Monthly bookkeeping." }, { ref: "INV-1036", cust: "Northgate Auto Body", due: "2026-07-30", amount: 7215.5, st: "Paid", memo: "Sales tax filing, second quarter." }, { ref: "INV-1034", cust: "Harbour Point Marina", due: "2026-09-19", amount: 2450, st: "Draft", memo: "Slip revenue review." }, ]; // SuiGridRow[] }

Basic

columns and rows are properties. A money column is aligned to the end in tabular numerals, and a column with render draws its own cell, here a link and a status pill.

<sui-data-table footer-meta="6 of 143 invoices"></sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('sui-data-table'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; }

Sorting

A sortable header cycles ascending, descending and off, and carries aria-sort as it goes. With sort-mode multiple a Shift+click adds a term and the headers number themselves. By default only the sorted column draws its arrow and the rest show theirs on hover; sort-indicator all, as here, draws every glyph at rest.

<sui-data-table id="dt-sort" sort-mode="multiple" sort-indicator="all" footer-meta="Sorted by status, then amount."></sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-sort'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; t.sorts = [{ key: 'st', dir: 'asc' }, { key: 'amount', dir: 'desc' }]; }

Filtering

filter-display row puts a box under each header that declares filter, and the search box in the header slot writes global-filter, which searches every visible column. A filter that leaves no rows shows empty-filter-message; empty-message is for no rows at all. filter-display menu gives a funnel button with match modes.

<sui-data-table id="dt-filter" filter-display="row" empty-filter-message="No invoices match."> <sui-input-text slot="header" id="dt-search" size="sm" placeholder="Search invoices"></sui-input-text> </sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-filter'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; root.querySelector('#dt-search').addEventListener('suiInput', (e) => { t.globalFilter = e.detail; }); }

Selection

selection-mode checkbox adds the box column and a select-all box. While a row is selected, the selection-actions slot takes the top bar at its height, so the rows never move; give it role="toolbar", a name and the count in words. Select-all stores "everything except these", so it survives the next page.

No invoices selected
<sui-data-table id="dt-sel" selection-mode="checkbox" aria-label="Invoices"> <sui-input-text slot="header" id="dt-sel-search" size="sm" aria-label="Search invoices" placeholder="Search invoices"></sui-input-text> <div slot="selection-actions" role="toolbar" aria-label="Selected invoices" style="flex:1;display:flex;align-items:center;gap:8px"> <span id="dt-sel-count" style="font:650 12.5px var(--font-sans)">No invoices selected</span> <sui-button id="dt-sel-paid" size="sm" variant="outline" label="Mark paid"></sui-button> <sui-button id="dt-sel-clear" size="sm" variant="ghost" label="Clear" style="margin-inline-start:auto"></sui-button> </div> </sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-sel'); const count = root.querySelector('#dt-sel-count'); let picked = []; t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; const clear = () => { t.selection = { mode: 'include', keys: new Set() }; picked = []; }; t.addEventListener('suiSelectionChange', (e) => { // include lists the selected keys; exclude, from select-all, lists the ones left out. const { mode, keys } = e.detail; picked = mode === 'include' ? [...keys] : t.rows.map(byRef).filter((k) => !keys.has(k)); const n = picked.length; count.textContent = mode === 'exclude' && keys.size === 0 ? 'All invoices selected' : `${n} ${n === 1 ? 'invoice' : 'invoices'} selected`; }); root.querySelector('#dt-sel-search').addEventListener('suiInput', (e) => { t.globalFilter = e.detail; }); root.querySelector('#dt-sel-paid').addEventListener('click', () => { t.rows = t.rows.map((r) => (picked.includes(r.ref) ? { ...r, st: 'Paid' } : r)); clear(); }); root.querySelector('#dt-sel-clear').addEventListener('click', clear); }

Expansion and grouping

row-detail adds the expander column and draws a panel under an open row. group-by puts a subheader over each group with the whole group's count and whatever group-summary returns. Rows are grouped first, sorted within the group (amount descending here) and paged last; a group past the page repeats its subheader.

<sui-data-table id="dt-group" aria-label="Invoices by status"> <sui-paginator slot="footer" page="1" page-size="4" hide-page-size></sui-paginator> </sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-group'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; t.sorts = [{ key: 'amount', dir: 'desc' }]; t.groupBy = 'st'; t.groupSummary = (rows) => asMoney(rows.reduce((s, r) => s + r.amount, 0)); t.rowDetail = (row) => { const el = root.ownerDocument.createElement('div'); el.style.cssText = 'font:460 12.5px/1.6 var(--font-sans);color:var(--ink-muted)'; el.textContent = row.memo || 'No memo on this invoice.'; return el; }; t.expandedRows = ['INV-1043']; }

Tree rows

row-tree maps each row key to its level and whether it is a branch, and the table becomes a treegrid: the first column indents and carries the twisty; a closed branch hides the rows under it. Hand it the whole outline in document order; expanded-rows names the open branches and suiExpandedRowsChange reports each turn.

<sui-data-table id="dt-tree" aria-label="Chart of accounts"></sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-tree'); t.columns = [ { key: 'name', header: 'Account', flex: 2 }, { key: 'code', header: 'Code', width: 90 }, { key: 'balance', header: 'Balance', money: true, align: 'end', format: asMoney }, ]; const rows = [ { key: 'assets', name: 'Assets', code: '1000', balance: 412650, level: 0, branch: true }, { key: 'cash', name: 'Cash and equivalents', code: '1010', balance: 145334, level: 1, branch: true }, { key: 'operating', name: 'Operating account', code: '1011', balance: 98200, level: 2 }, { key: 'payroll', name: 'Payroll account', code: '1012', balance: 47134, level: 2 }, { key: 'receivable', name: 'Accounts receivable', code: '1100', balance: 267316, level: 1 }, { key: 'liabilities', name: 'Liabilities', code: '2000', balance: 88410, level: 0, branch: true }, { key: 'payable', name: 'Accounts payable', code: '2010', balance: 61250, level: 1 }, { key: 'accrued', name: 'Accrued payroll', code: '2020', balance: 27160, level: 1 }, ]; t.rows = rows; t.rowKey = (r) => r.key; t.rowTree = new Map(rows.map((r) => [r.key, { level: r.level, expandable: !!r.branch }])); t.expandedRows = ['assets', 'cash']; }

Frozen columns

A column with pin start or pin end stays put while the rest scrolls sideways, and view-height keeps the header stuck to the top of the rows. gridlines draws the vertical rules.

<sui-data-table id="dt-frozen" gridlines view-height="260px"></sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-frozen'); const cols = invoiceColumns(root.ownerDocument); cols[0] = { ...cols[0], pin: 'start' }; cols[3] = { ...cols[3], pin: 'end' }; cols.splice(3, 0, { key: 'terms', header: 'Terms', width: 130 }, { key: 'owner', header: 'Owner', width: 170 }, { key: 'po', header: 'PO number', width: 150 }, ); t.columns = cols; t.rows = invoices().map((r, i) => ({ ...r, terms: ['Net 30', 'Net 15', 'Due on receipt'][i % 3], owner: ['Dana Whitfield', 'Marcus Reed', 'Priya Anand'][i % 3], po: `PO-4${400 + i}` })); t.rowKey = byRef; }

Column layout

A resizable column has a drag handle on its end edge, reorderable-columns lets a header be dragged to a new place, and show-column-toggle adds the columns button. All three write one layout object.

<sui-data-table id="dt-layout" reorderable-columns show-column-toggle footer-meta="Drag a header to move it, drag its edge to resize it."></sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-layout'); t.columns = invoiceColumns(root.ownerDocument).map((c) => ({ ...c, resizable: true })); t.rows = invoices(); t.rowKey = byRef; }

Editing

edit-mode cell opens one editor on double click or F2; Enter commits, Escape cancels. A column names its control with an editor, and a validator can refuse it: clear the customer and the message stays until it passes. The table never writes the record: suiCellEdit reports it and the host hands back fresh rows.

Double click a customer or an amount.
<div style="display:grid;gap:10px"> <code id="dt-edit-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Double click a customer or an amount.</code> <sui-data-table id="dt-edit" edit-mode="cell"></sui-data-table> </div>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-edit'); const log = root.querySelector('#dt-edit-log'); const input = { mount: (ctx) => { const el = root.ownerDocument.createElement('sui-input-text'); el.size = 'sm'; el.value = ctx.text; return el; }, read: (el) => String(el.value == null ? '' : el.value), }; const cols = invoiceColumns(root.ownerDocument); cols[1] = { ...cols[1], editor: input, validator: (v) => (v.trim() ? null : 'Enter a customer.') }; cols[3] = { ...cols[3], editor: input }; t.columns = cols; t.rows = invoices(); t.rowKey = byRef; t.addEventListener('suiCellEdit', (e) => { const { rowKey, columnKey, value } = e.detail; const next = t.rows.map((r) => (byRef(r) === rowKey ? { ...r, [columnKey]: columnKey === 'amount' ? Number(value) : value } : r)); t.rows = next; log.textContent = `${rowKey}: ${columnKey} is now ${value}.`; }); }

Virtual rows

virtual mounts only the rows near the viewport and two spacer rows for the rest, so five thousand invoices cost a screenful of DOM. Every line is virtual-row-height tall, which is the price of the window.

<sui-data-table id="dt-virtual" virtual virtual-row-height="36" view-height="300px" density="compact" footer-meta="5,000 invoices, a screenful in the document."></sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-virtual'); t.columns = invoiceColumns(root.ownerDocument); t.rows = bulk; t.rowKey = byRef; }

Totals and paging

footer-totals takes one string per column key, so a reordered or hidden column keeps its number. A sui-paginator in the footer slot is wired up: the table keeps its total in step with the filter and takes the page from it. At one page the paginator hides, and takes the footer bar with it when it is all the bar holds.

<sui-data-table id="dt-totals" aria-label="Invoices with totals" footer-meta="Filtered to the last 90 days."> <sui-paginator slot="footer" page="1" page-size="4" hide-page-size></sui-paginator> </sui-data-table>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-totals'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; t.footerTotals = { cust: 'Total', amount: asMoney(invoices().reduce((s, r) => s + r.amount, 0)) }; }

Inside a card

bare drops the table's own border and corners, so the card is the one frame. A flush card hands its 20px inset (--sui-card-pad-x) to the table's first and last columns and its bars, so they start on the heading's line.

<sui-card heading="Invoices" flush> <sui-button slot="header-end" variant="link" size="sm" label="View all"></sui-button> <sui-data-table id="dt-card" bare aria-label="Invoices" footer-meta="6 of 143 invoices"></sui-data-table> </sui-card>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-card'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices(); t.rowKey = byRef; }

Stacked on narrow screens

Under breakpoint pixels of frame width each row becomes a card and every cell takes its column heading as a label. Narrow the window, or drag the splitter, to watch it turn over.

<div style="max-width:420px"><sui-data-table id="dt-stack" breakpoint="520"></sui-data-table></div>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-stack'); t.columns = invoiceColumns(root.ownerDocument); t.rows = invoices().slice(0, 3); t.rowKey = byRef; }

Loading and empty

loading draws four shimmer rows, marks the body aria-busy and says Loading to a reader. With no rows the table shows empty-message and the empty-action slot, which offers the next step rather than an apology; the empty slot replaces the sentence itself, as on the grid, list and tree. The message carries role status.

Enter a bill to get started No bills are due this week.
<div style="display:grid;gap:16px"> <sui-data-table id="dt-load" loading></sui-data-table> <sui-data-table id="dt-empty" empty-message="No open bills."><a slot="empty-action" href="#">Enter a bill to get started</a></sui-data-table> <sui-data-table id="dt-empty-slot"><span slot="empty">No bills are due this week.</span></sui-data-table> </div>
// setup(root): properties and events setup(root) { const cols = [ { key: 'ref', header: 'Bill #' }, { key: 'vend', header: 'Vendor' }, { key: 'amount', header: 'Amount', money: true }, ]; root.querySelector('#dt-load').columns = cols; root.querySelectorAll('#dt-empty, #dt-empty-slot').forEach((e) => { e.columns = cols; e.rows = []; }); }

Events

A row click emits suiRowClick, a right click suiRowContextMenu with the pointer position, a focused row suiRowFocus. Headers, boxes and filter menus report through suiSort, suiSelectionChange and suiFilter; suiViewWidthChange carries the scroller width. Row actions bubble; state reports do not, so listen on the table.

Nothing yet.
<div style="display:grid;gap:10px"> <code id="dt-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing yet.</code> <sui-data-table id="dt-events" selection-mode="multiple" filter-display="menu"></sui-data-table> </div>
// setup(root): properties and events setup(root) { const t = root.querySelector('#dt-events'); const log = root.querySelector('#dt-log'); const rows = invoices(); const states = [...new Set(rows.map((r) => r.st))]; t.columns = invoiceColumns(root.ownerDocument).map((c) => (c.key === 'st' ? { ...c, filterOptions: states } : c)); t.rows = rows; t.rowKey = byRef; t.addEventListener('suiRowClick', (e) => { log.textContent = `Row ${e.detail.key}: ${e.detail.row.cust}.`; }); t.addEventListener('suiRowFocus', (e) => { log.textContent = `Focus on ${e.detail.key}, row ${e.detail.index + 1}.`; }); t.addEventListener('suiRowContextMenu', (e) => { log.textContent = `Context menu on ${e.detail.key} at ${e.detail.x}, ${e.detail.y}.`; }); t.addEventListener('suiSelectionChange', (e) => { log.textContent = `Selected ${e.detail.mode === 'include' ? e.detail.keys.size : t.rows.length - e.detail.keys.size} of ${t.rows.length}.`; }); t.addEventListener('suiSort', (e) => { log.textContent = e.detail.length === 0 ? 'Sort cleared.' : `Sort ${e.detail.map((s) => `${s.key} ${s.dir}`).join(', ')}.`; }); t.addEventListener('suiFilter', (e) => { log.textContent = `Filter on ${Object.keys(e.detail.rules || {}).join(', ') || 'nothing'}.`; }); t.addEventListener('suiViewWidthChange', (e) => { log.textContent = `Scroller ${e.detail}px wide.`; }); }

Keyboard

KeyAction
TabSteps through the columns button, each sortable header, the filter boxes, the row body as a single stop, and then the footer controls.
ArrowDown ArrowUpMoves the focused row, and scrolls it into view when the rows are virtual.
Home EndFocuses the first or the last row on the page.
PageUp PageDownMoves the focused row by one viewport.
SpaceSelects or clears the focused row, unless the selection mode is none.
EnterActivates the focused row and emits suiRowClick.
ArrowRight ArrowLeftOpens or closes the focused row when there is a row detail, or opens and closes a branch under row-tree, where the back arrow on a leaf moves to its parent. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
F2Opens the editor on the focused row, or on its first editable cell in cell mode.
Alt+ArrowDownOn a header, opens the column filter menu when filter-display is menu.
Enter Space on a filter-row mode buttonOpens the list of match modes for that column; the arrows move through it and Enter picks one.
Enter in a filter menuApplies the menu from a text or number field. Tab walks the menu controls; past the last one it closes the menu and carries on from the funnel.
EscapeCancels an edit, and closes the columns or filter menu without applying it. Inside the menu, an open list or calendar closes first.
Shift+clickOn a header, adds a sort term when sort-mode is multiple. On a row, selects the run from the last one.

API

<sui-data-table>

Properties

PropertyAttributeTypeDefaultDescription
barebarebooleanfalseDrops the table's own border and corners, for a table that fills a sui-card (or any frame of the page's) edge to edge: the card is then the one frame, where two borders a pixel apart doubled the edge and the table's rounded corners stopped short of the card's. Leave it off where the table sits inside padded content and needs its own frame. Reflected.
breakpointbreakpointnumber0Frame width in px under which each row becomes a stacked card with its header as a label per cell. Zero leaves the table as a table at every width.
columnLayoutproperty onlySuiGridLayout{}Column order, dragged widths and hidden columns. Mutable; a resize, a reorder and the column menu all rewrite it before suiColumnLayoutChange fires. The same shape sui-data-grid takes. Property, not attribute.
columnsproperty onlySuiDataTableColumn[][]Column definitions. key is the identity and the field read from the row; width and flex size the column, render draws the cell, pin freezes it against an edge. Property, not attribute.
densitydensitySuiGridDensity"comfortable" | "compact" | "standard"'standard'Row height and cell padding.
editModeedit-mode"cell" | "none" | "row"'none'Whether a double click or F2 opens one cell's editor, every editor in the row, or nothing. A column takes part only if it declares an editor. Enter commits and Escape cancels.
emptyFilterMessageempty-filter-messagestringstring | undefinedText for a table whose rows the filter row, the filter menu or globalFilter narrowed to none, and under lazy for no rows while a filter is set. An empty list and a search that found nothing are two states, so this is a second prop; an empty string silences it the same way.
emptyMessageempty-messagestringstring | undefinedText for a table with no rows at all. An empty string draws the empty row without words, for a host that says it once above the table. Never apologizes; pair it with the empty-action slot.
expandedRowsproperty onlySuiKey[][]Keys of the expanded rows. Mutable; the expander button rewrites it before suiExpandedRowsChange fires. Property, not attribute.
filterDisplayfilter-display"menu" | "none" | "row"'none'Where the filter controls live: nowhere, in a row under the headers, or behind a funnel button on each header's end edge. The menu offers up to two conditions suited to the column's type, joined by all or any, or a list of values to tick when the column sets filterOptions; it edits a draft and writes it on Apply.
filterModelproperty onlySuiGridFilterModelSuiGridFilterModel | undefinedThe filter model: one rule per column plus an optional global term. Named for the model, since filter on the list elements is the boolean that shows a search box; the table's boxes are filterDisplay. Mutable; the filter row and the filter menu rewrite it before suiFilter fires. Property, not attribute.
footerMetafooter-metastringstring | undefinedFaint meta line at the foot of the frame ("4 of 143 rows").
footerTotalsproperty onlystring | undefinedFooter totals, by column key, so a reordered or hidden column keeps its total. Property, not attribute.
globalFilterglobal-filterstringstring | undefinedOne term searched across several columns at once. Set it from a search box in the header slot; it is merged into filterModel as the global term.
globalFilterFieldsproperty onlystring[]string[] | undefinedColumns the global term searches. Defaults to every visible column; name them to keep an expensive computed column out of the search. Property, not attribute.
gridlinesgridlinesbooleanfalseDraws a rule between columns as well as between rows.
groupBygroup-bystringstring | undefinedColumn key to group by. The rows are ordered by it first, ascending unless sorts names it, then by the sort terms within each group, and paged last, so a second sort never splits a group and a page never shows a slice of the grouping. A subheader over each group carries the whole group's count and summary; a group that runs past the end of a page repeats its subheader at the top of the next. Under lazy the host orders and pages the rows, and the table groups what arrives.
groupSummaryproperty only((rows: SuiGridRow[], value: unknown) => string) | undefinedText drawn at the end of a group subheader, given that group's rows and its value. Property, not attribute.
hideSelectionBarhide-selection-barbooleanfalseLeaves a selected row its tint alone, without the accent bar the table draws at its start, as sui-list draws it, so that a selection reads without leaning on the tint.
lazylazybooleanfalseThe host owns filtering, sorting and paging, and the table renders rows as they arrive. Turn it on for a server-backed table and act on suiSort, suiFilter and suiPageChange.
loadingloadingbooleanfalseRenders four shimmer rows in place of data, marks the body aria-busy and names the wait in a status line a reader hears. Reflected, so a page can style the wait.
nullOrdernull-orderSuiGridNullOrder"first" | "last"'last'Where blanks land, in both directions. last keeps the empty rows out of the way whichever way the column is sorted.
overscanoverscannumber6Rows kept mounted beyond each edge of the viewport while virtual is on. The window leans into the direction of travel, so this is a floor rather than a fixed count.
pagepagenumber1Current page, 1-based. Mutable; a sui-paginator in the footer slot writes it.
pageSizepage-sizenumber0Rows per page. Zero shows every row, which is the default: the frame scrolls instead. A page counts data rows only; the group subheaders over them are lines on top of the count.
reorderableColumnsreorderable-columnsbooleanfalseLets a header be dragged to a new position.
rowDetailproperty only((row: SuiGridRow, key: SuiKey) => SuiGridCellValue) | undefinedDetail panel under an expanded row. Setting it adds the expander column. Property, not attribute.
rowKeyproperty only((row: SuiGridRow, index: number) => SuiKey) | undefinedRow identity (row, index) => key; defaults to the row index. Selection, expansion and the edit events all carry this key, so a table whose rows reorder needs a real one. Property, not attribute.
rowPartialproperty onlyReadonlySet<SuiKey> | undefinedRows whose checkbox draws as mixed. The table paints this state and never infers it: what makes a row partly selected is the shape of the caller's data, not anything the table holds. Property, not attribute.
rowTreeproperty onlyReadonlyMap<SuiKey, SuiDataTableRowTree> | undefinedWhere each row sits in a hierarchy, by row key. Setting it makes the table a treegrid: the first column indents and carries the expander, the rows take aria-level and aria-expanded, and the forward and back arrows walk the levels. Hand it the whole outline in rows, in document order: the table skips the rows under a closed branch itself, so a collapse hides the descendants without the host rewriting rows on suiExpandedRowsChange. Needs a rowKey, since the map is keyed by it. A Map rather than a callback, so a rendered row costs one lookup and no allocation. A flex first column keeps room for the deepest indent and about a dozen characters, so a narrow table scrolls sideways before it cuts a nested name. Property, not attribute.
rowsproperty onlySuiGridRow[][]Row records, read by column key. Pass a new array to change the data: the filter and sort pipeline is memoized on identity, so a mutated array returns the previous answer. Property, not attribute.
selectionproperty onlySuiGridRowSelectionSuiGridExcludeSelection | SuiGridIncludeSelectionclearSelection()Which rows are selected. include lists the selected keys; exclude means everything except the listed keys, which is how select-all survives a page turn over data the client does not hold. Mutable. Property, not attribute.
selectionModeselection-mode"checkbox" | "multiple" | "none" | "single"'none'How rows are selected: not at all, one at a time, many by Shift and Ctrl or Cmd, or many with a checkbox column and a select-all box.
showColumnToggleshow-column-togglebooleanfalseAdds the columns button above the table, which turns columns off and on.
sortIndicatorsort-indicator"all" | "sorted"'sorted'Which sortable headers draw a glyph at rest. sorted draws the arrow on the sorted column only, and the other sortable headers show theirs on hover and on focus; all draws a glyph on every sortable header.
sortModesort-mode"multiple" | "single"'single'Whether a header click replaces the sort or adds to it. Under multiple a Shift+click adds a term; under either, a third click on a column drops it.
sortsproperty onlySuiGridSort[][]Sort terms, applied in order: the first decides and ties fall to the next. Mutable; a header click rewrites it before suiSort fires. Property, not attribute.
stateKeystate-keystringstring | undefinedStorage key for the sorts, filters, column layout and page. Leave it unset and the table remembers nothing. On first load a value the host set wins over the saved one and a prop still at its default takes the saved value, so a search or a sort the page set before the table loaded survives; the merged arrangement is written back so the next load agrees with it. Changing the key later loads the whole arrangement saved under the new one.
stateStoragestate-storage"local" | "session"'session'Where stateKey writes: this tab only, or across tabs and restarts.
stripedstripedbooleanfalseTints every second row.
totalRecordstotal-recordsnumbernumber | undefinedTotal rows behind a lazy table, for the row count and the paginator.
viewHeightview-heightstringstring | undefinedCaps the height of the scrolling rows area, as a CSS length such as 360px or 50vh. A bare number is read as pixels. The header stays stuck to its top.
virtualvirtualbooleanfalseRenders only the rows near the viewport. Uniform row height only: every line, including group subheaders and expanded detail panels, is virtualRowHeight tall. Needs viewHeight or a height on the host, because a window with no viewport has nothing to window against.
virtualRowHeightvirtual-row-heightnumber0Row height in px while virtual is on. Zero takes the height the density implies.

Events

EventDetailReactVueDescription
suiCellEditSuiGridCellEditDetailonSuiCellEdit@sui-cell-editFires once per changed column when an edit commits. Detail is a SuiGridCellEditDetail, the shape sui-data-grid reports too: { data, rowKey, sourceIndex, rowIndex, columnKey, value, previous }. The table does not write the row, so apply the value through rowKey and hand back a new rows array.
suiColumnLayoutChangeSuiGridLayoutonSuiColumnLayoutChange@sui-column-layout-changeFires after a column resize, reorder or toggle; detail is the whole columnLayout. Does not bubble.
suiExpandedRowsChangeSuiKey[]onSuiExpandedRowsChange@sui-expanded-rows-changeFires when a row opens or closes; detail is the new expandedRows. Does not bubble.
suiFilterSuiGridFilterModelonSuiFilter@sui-filterFires when the filter row, the filter menu or the global term changes the filter; detail is the whole model. Does not bubble.
suiPageChangenumberonSuiPageChange@sui-page-changeFires when the page changes, by the paginator or by a page size that moved it; detail is the new page. pageSize is already written, so a lazy host reads both here. Does not bubble.
suiPageSizeChangenumberonSuiPageSizeChange@sui-page-size-changeFires when the paginator's rows-per-page control changes the page size; detail is the new pageSize. A page size that moves the page writes both props, then fires this and suiPageChange. Does not bubble.
suiRowClickSuiDataTableRowEventDetailonSuiRowClick@sui-row-clickFires when a row is clicked or activated with Enter; detail is { row, key, index, x, y }.
suiRowContextMenuSuiDataTableRowEventDetailonSuiRowContextMenu@sui-row-context-menuFires on a right click over a row; detail carries the pointer position so a sui-context-menu can open there. The browser's own menu is suppressed.
suiRowFocusSuiDataTableRowEventDetailonSuiRowFocus@sui-row-focusFires when a row, or a control inside it such as its checkbox, receives focus; detail is { row, key, index, x, y } with x and y zero. Focus moving between a row and its own controls does not fire it again. A host opens a menu or runs a shortcut for the focused row from this rather than by reading a cell. Does not bubble.
suiSelectionChangeSuiGridRowSelectionSuiGridExcludeSelection | SuiGridIncludeSelectiononSuiSelectionChange@sui-selection-changeFires when the selection changes; detail is the new selection. Under mode: 'include' its keys are the selected rows; under mode: 'exclude' every row is selected except its keys, which is how select-all reads over rows the page does not hold. Does not bubble.
suiSortSuiGridSort[]onSuiSort@sui-sortFires when a header click changes the sort; detail is the new ordered term list. Does not bubble.
suiViewWidthChangenumberonSuiViewWidthChange@sui-view-width-changeFires when the scroller's client width changes, the first measurement after load included; detail is the width in px, what getViewWidth() answers. It comes through the table's own ResizeObserver, so it is at most one per frame, and a host that hides or sizes columns from it writes columnLayout in the frame the width changed, which the table already allows for. Does not bubble.

Methods

MethodSignatureDescription
exportCsv()exportCsv(filename?: string) => Promise<boolean>Downloads the filtered, sorted rows as CSV, in the column order and visibility on screen. Values run through each column's value or field and then its format, so the file says what the table says. Text a spreadsheet would read as a formula is escaped, because the file is opened by someone who did not watch it being made.
getViewWidth()getViewWidth() => Promise<number>The scroller's client width right now, for a caller that needs it outside suiViewWidthChange. Zero before the first measurement.
scrollToRow()scrollToRow(index: number, align?: "auto" | "start" | "center" | "end") => Promise<void>Scrolls until the row at index is in view. align forces a position even when it already is, which is what a jump-to-row control wants.
setFilter()setFilter(model: SuiGridFilterModel) => Promise<void>Applies a whole filter model the way the filter row and the menus do: it becomes filterModel, the table returns to page 1, the model is stored under stateKey, and it goes out on suiFilter, followed by suiPageChange when the table was on another page. What sui-filter-builder calls.
setFocus()setFocus() => Promise<void>Focuses the row that holds the table's one tab stop: the row focused last, or the first row in view. With no rows, it focuses the first control in the header. el.focus() on the host does the same.

Slots

SlotDescription
emptyReplaces the empty sentence when there are no rows at all, the slot every list in the family takes; a filter that finds nothing keeps emptyFilterMessage.
empty-actionLink or button under the empty message offering the next step. It stays when the message is an empty string.
footerControls under the rows, inside the frame. A sui-paginator here is wired up: the table keeps its totalRecords in step with the filtered count and adopts its page.
headerControls above the table: a search box, a sui-filter-builder, a filter chip row.
selection-actionsWhat to do with the selected rows: the count, Change status, Delete, Clear. While any row is selected it takes the header's place in the top bar, Columns button included, and the bar keeps the taller of the two heights, so the rows never move when a selection starts or ends. Give it a role="toolbar" and an aria-label, and say the count in words.

Types

SuiDataTableColumn interfaceA column, plus the three things a table does that a grid does not: filter it from the header, edit it in place, and total it in the footer. Everything else comes from SuiGridColumn.
keystringUnique identity, and the row field read unless field or value says otherwise.
headerstringColumn heading.
headerSr?stringHeading read by assistive technology when header is empty: an actions column shows no words and still names itself. Ignored when header has text.
field?stringRow field to read, when it is not key.
value?(row: SuiGridRow) => unknownComputed value for sorting, filtering and export. Takes precedence over field; a column with only a render and no value sorts and exports by its raw field, which is usually wrong.
render?(row: SuiGridRow, key: SuiKey) => SuiGridCellValueCell content. A string renders as text; an element is mounted into the cell. Called only for cells inside the rendered window. Property, not attribute.
type?SuiGridValueTypeDrives the default comparator, the default filter match modes and the default alignment.
align?SuiGridAlignA side of the screen to align to, in either reading direction. Leave it unset for reading order: text at the inline start, numbers and money at the inline end.
money?booleanAligned to the inline end, tabular numerals, weight 640. Implies type: 'number' for comparison unless the column says otherwise, because a money column holds an amount.
nowrap?booleanOne line, clipped with an ellipsis, and the full value on the cell's title.
width?numberStarting width in px. Omit to let flex share the space.
minWidth?number
maxWidth?number
flex?numberShare of the space left after fixed widths. flex: 1 on two columns splits it evenly.
sortable?boolean
resizable?booleanDraggable edge on the header. Off by default; a resized column drops its flex.
hidden?booleanExcluded from the render, from export and from the keyboard, but kept in the model so a column toggle can bring it back without the caller rebuilding the array.
pin?SuiGridPinPins the column to an edge. Pinned columns are never windowed away.
compare?(a: unknown, b: unknown) => numberCustom comparator; overrides type. Return negative, zero or positive.
format?(value: unknown, row: SuiGridRow) => stringFormats the value for text: the cell when there is no render, the export, and the clipboard. Keep it cheap; it runs once per visible cell per render. Without it a number or money column shows a plain figure grouped and marked in the page's locale (1,234.5, 1.234,5), while the export, the clipboard and an edit keep the plain figure (1234.5).
muted?booleanDrawn in --ink-muted: a memo, an account type, anything secondary.
headerTitle?stringHeader tooltip, for a column whose heading had to be short.
editable?booleanOpts a column out of in-cell editing on a grid that has it on. A column that draws its own cell is never editable: there is no text for an editor to stand in for.
cellClass?(value: unknown, row: SuiGridRow, index: number) => string | undefinedExtra classes for this column's cells, per row. Called once per visible cell per render, so keep it cheap: return a stable string rather than building one. The grid's own classes are always applied; this adds to them.
filter?booleanOffers this column a filter control, in the filter row or the filter menu. Ignored while the element's filterDisplay is none.
matchMode?SuiGridMatchModeMatch mode the column's filter opens with. Defaults by type: contains for text, equals for numbers and booleans, dateIs for dates.
filterOptions?readonly (string | { value: string; label: string; })[]The values a status-like column holds. With them, the column's filter menu is a checklist of these values, writing an in rule, instead of a list of conditions. A bare string is its own value and label. The filter row ignores them and keeps its text box.
editor?SuiGridEditorThe control this column opens for editing. A plain text input when it is absent. Named by value rather than by a string, so an application that edits only text never carries the date picker: see the note at the top of editing.ts.
validator?SuiGridValidatorChecked when an edit commits. Returning a message refuses the commit and keeps the editor open with the message beside it.
SuiDataTableRowEventDetail interfaceWhat suiRowClick, suiRowContextMenu and suiRowFocus carry. x and y are viewport coordinates and are zero for a click or a focus, which need no position.
rowSuiGridRow
keySuiKey
indexnumberPosition in the rows on screen, after filtering, sorting and paging.
xnumber
ynumber
SuiDataTableRowTree interfaceWhere one row sits in a hierarchy. Supplying rowTree is the whole of the table's tree affordance: it draws the indent, the expander, the treegrid roles and the levels, and it computes none of them. Node types, checkbox propagation and lazy children belong to sui-tree-table and to utils/grid/tree.ts.
levelnumberZero at the root. Drives the indent and aria-level, which is one-based, so the table adds one.
expandablebooleanTrue for a branch, including one whose children have not arrived. A branch draws the twisty and answers the arrow keys; a leaf keeps the indent and nothing else.
loading?booleanThe branch is fetching its children: the twisty becomes a spinner and the row is aria-busy.
size?numberSiblings this row has, including itself, for aria-setsize. Omit both this and pos and the table writes neither, because a reader is better served by silence than by a made-up count.
pos?numberThis row's one-based place among those siblings, for aria-posinset.
SuiGridAlign typeCell text alignment. A side named here is a side of the screen in either reading direction. Left unset, a money or number column sits at the inline end and everything else at the inline start, so the default mirrors under dir="rtl".
No members.
SuiGridCellEditDetail interfaceOne committed cell edit: the detail of suiCellEdit on sui-data-grid and on sui-data-table. Neither element writes the row. It reports the edit and the host applies it, the same props-in, events-out contract every element here keeps. Each field says what it holds, so no name means a record in one element and a position in the other: the record is data, its identity rowKey, its place in rows sourceIndex, its place on screen rowIndex, and the column columnKey. Write through rowKey or sourceIndex, which survive a sort and a filter; rowIndex names a different record as soon as the view reorders.
dataSuiGridRowThe record, as the element read it.
rowKeySuiKeyrowKey(data). An element with no rowKey keys a row by its index, which a sort moves, so a host that writes edits back should give it one.
sourceIndexnumberThe record's index in rows, or -1 when it is no longer there.
rowIndexnumberThe row's position on screen, after filtering, sorting and paging; -1 when a grid's undo or redo lands on a record the filter now hides.
columnKeystringThe edited column's key.
valuestringThe new text, as the cell shows text and as the column's editor reads it: a formatted column's editor opens on its formatted text, so parse it back for a number or a date.
previousstringThe text the cell showed before, in the same form.
source?'undo' | 'redo'Where the edit came from. The grid replays an undo and a redo as ordinary edits with the values the other way round, and says so here: a host that logs changes, or writes them to a server, usually treats a correction differently from a fresh entry. Absent means a person typed it.
SuiGridCellValue typeWhat a cell renderer may return. A string is text; an element is appended to the cell. Shared with utils/render-content.ts, which does the mounting.
No members.
SuiGridColumn interfaceOne column. key is both the field read from the row and the column's identity, so it must be unique across the set; field overrides the field when the two need to differ (two columns over the same field, or a computed column with no field at all).
keystringUnique identity, and the row field read unless field or value says otherwise.
headerstringColumn heading.
headerSr?stringHeading read by assistive technology when header is empty: an actions column shows no words and still names itself. Ignored when header has text.
field?stringRow field to read, when it is not key.
value?(row: SuiGridRow) => unknownComputed value for sorting, filtering and export. Takes precedence over field; a column with only a render and no value sorts and exports by its raw field, which is usually wrong.
render?(row: SuiGridRow, key: SuiKey) => SuiGridCellValueCell content. A string renders as text; an element is mounted into the cell. Called only for cells inside the rendered window. Property, not attribute.
type?SuiGridValueTypeDrives the default comparator, the default filter match modes and the default alignment.
align?SuiGridAlignA side of the screen to align to, in either reading direction. Leave it unset for reading order: text at the inline start, numbers and money at the inline end.
money?booleanAligned to the inline end, tabular numerals, weight 640. Implies type: 'number' for comparison unless the column says otherwise, because a money column holds an amount.
nowrap?booleanOne line, clipped with an ellipsis, and the full value on the cell's title.
width?numberStarting width in px. Omit to let flex share the space.
minWidth?number
maxWidth?number
flex?numberShare of the space left after fixed widths. flex: 1 on two columns splits it evenly.
sortable?boolean
resizable?booleanDraggable edge on the header. Off by default; a resized column drops its flex.
hidden?booleanExcluded from the render, from export and from the keyboard, but kept in the model so a column toggle can bring it back without the caller rebuilding the array.
pin?SuiGridPinPins the column to an edge. Pinned columns are never windowed away.
compare?(a: unknown, b: unknown) => numberCustom comparator; overrides type. Return negative, zero or positive.
format?(value: unknown, row: SuiGridRow) => stringFormats the value for text: the cell when there is no render, the export, and the clipboard. Keep it cheap; it runs once per visible cell per render. Without it a number or money column shows a plain figure grouped and marked in the page's locale (1,234.5, 1.234,5), while the export, the clipboard and an edit keep the plain figure (1234.5).
muted?booleanDrawn in --ink-muted: a memo, an account type, anything secondary.
headerTitle?stringHeader tooltip, for a column whose heading had to be short.
editable?booleanOpts a column out of in-cell editing on a grid that has it on. A column that draws its own cell is never editable: there is no text for an editor to stand in for.
cellClass?(value: unknown, row: SuiGridRow, index: number) => string | undefinedExtra classes for this column's cells, per row. Called once per visible cell per render, so keep it cheap: return a stable string rather than building one. The grid's own classes are always applied; this adds to them.
filter?booleanOffers this column a filter control, in the filter row or the filter menu. Ignored while the element's filterDisplay is none.
matchMode?SuiGridMatchModeMatch mode the column's filter opens with. Defaults by type: contains for text, equals for numbers and booleans, dateIs for dates.
filterOptions?readonly (string | { value: string; label: string; })[]The values a status-like column holds. With them, the column's filter menu is a checklist of these values, writing an in rule, instead of a list of conditions. A bare string is its own value and label. The filter row ignores them and keeps its text box.
editor?SuiGridEditorThe control this column opens for editing. A plain text input when it is absent. Named by value rather than by a string, so an application that edits only text never carries the date picker: see the note at the top of editing.ts.
validator?SuiGridValidatorChecked when an edit commits. Returning a message refuses the commit and keeps the editor open with the message beside it.
SuiGridDensity typeRow height and cell padding. The three stops are the library's control heights, so a grid row lines up with the inputs beside it.
No members.
SuiGridEditor interfaceA control the grid mounts into a cell, focuses, and reads back when the edit commits. In sui-data-grid focus leaving the control commits, and focus in the list or calendar the control opened has not left it, so a ShineUI picker works as an editor.
mount(ctx: SuiGridEditorContext) => HTMLElementBuilds the control, populated and ready. The grid mounts it in the cell and focuses it.
read(el: HTMLElement) => stringThe text to commit, read from the element mount returned.
handlesKey?(e: KeyboardEvent, el: HTMLElement) => booleanOptional: keys this editor consumes itself, so the grid does not also act on them.
SuiGridEditorContext interfaceWhat an editor is given when it opens.
valueunknownThe raw value from the row.
textstringWhat the cell was showing, which is what a text editor seeds with.
columnSuiGridColumn
rowSuiGridRow
seed?stringSeed text when the editor was opened by a printable keystroke, else undefined.
SuiGridExcludeSelection interfaceEverything is selected except these keys.
mode'exclude'
keysReadonlySet<SuiKey>
SuiGridFilterCondition interfaceOne condition of an advanced filter: a column, a mode and a value, the same test a constraint makes, with the column it reads named beside it.
keystring
matchModeSuiGridMatchMode
value?unknown
SuiGridFilterConstraint interfaceOne test. value carries whatever the mode needs: a scalar, an array for in and notIn, a two-element [from, to] for between, and nothing at all for empty and notEmpty.
value?unknown
matchModeSuiGridMatchMode
SuiGridFilterGroup interfaceConditions and groups joined by one operator, to any depth: "Owner is Davon, and any of (health is Critical, no next meeting, no interaction)". What sui-filter-builder edits, and the one shape in the model that can join two columns with or.
operator'and' | 'or'
items(SuiGridFilterCondition | SuiGridFilterGroup)[]
SuiGridFilterModel interfaceThe whole filter state. Rules are keyed by column so a header can ask "is this column filtered" in one lookup; the global term is a sibling rather than a magic key inside the map, because it is a different shape and a map that holds two shapes cannot be typed. tree holds what only a tree can say, conditions across columns joined by or or nested; the three are joined by and.
rules?Readonly<Record<string, SuiGridFilterRule>>
global?SuiGridGlobalFilter
tree?SuiGridFilterGroup
SuiGridFilterRule interfaceEvery constraint on one column, and how they combine. and is the default a filter menu opens with; or is what a user reaches for when one column can hold two acceptable answers.
keystringThe column this rule filters. It is repeated inside the rule, rather than being left to the map key in SuiGridFilterModel, because a rule also travels alone in an event detail and in saved state, where there is no map key to read. The evaluator reads this field, so the two can never quietly disagree about which column a rule applies to.
operator'and' | 'or'
constraintsSuiGridFilterConstraint[]
SuiGridGlobalFilter interfaceOne term searched across several columns at once. keys is required rather than defaulting to every column, for the same reason PrimeVue makes globalFilterFields explicit: a computed column's value function would otherwise be called once per row per keystroke without anyone having asked for it.
valuestring
keysreadonly string[]
matchMode?SuiGridMatchModeDefaults to contains.
SuiGridIncludeSelection interfaceThe keys that are selected.
mode'include'
keysReadonlySet<SuiKey>
SuiGridLayout interfaceThe arrangement a user has made of the columns, and the shape both elements persist. It lives here rather than on either element because a saved layout has to survive being handed from one to the other: a team that outgrows the table and moves a screen to the grid should not have to migrate the column widths their users dragged. Keys the order does not name keep their declared position behind the ones it does, so adding a column in code never invalidates a layout saved before that column existed.
order?string[]Column keys in display order.
widths?Record<string, number>Dragged widths in px, by column key. A dragged column is fixed and drops its flex.
hidden?string[]Column keys a column toggle has turned off.
SuiGridMatchMode typeHow one constraint tests a value. Declared here rather than in filter.ts because a column names one and every element reads it, which is what this file is for; the evaluator that implements them is in filter.ts, which re-exports this so both can be imported together. The first sixteen are PrimeVue's FilterMatchMode verbatim. Three are additions, and they are additions rather than oversights: notIn because in without its negation is an odd half of a pair, and empty / notEmpty because "rows with no memo" is the filter a ledger reaches for most and PrimeVue cannot express it at all (an empty filter value means "no filter" there, so emptiness is exactly the one thing a value-carrying constraint can never ask about).
No members.
SuiGridNullOrder typeWhere nulls and blanks land, independent of direction: last keeps empties out of the way in both directions, which is what a ledger wants.
No members.
SuiGridPin typeWhich edge a column is pinned to, if any. The grid renders a pane per edge; the table uses position: sticky cells, because a native table cannot be split into panes.
No members.
SuiGridRow typeA row is the host's own record. The elements read fields by column key, print them and compare them; they never assume a shape beyond that.
No members.
SuiGridRowSelection typeWhich rows are selected. exclude is not a convenience. It is the only honest way to express select-all over a dataset whose total the client does not hold: a server-backed grid matching four million rows cannot enumerate them, and an allSelected boolean beside a key set falls apart the moment the user unticks three of them. "Everything except these three" survives that, survives paging, and is what a bulk action posts to a server as a predicate rather than as a list.
No members.
SuiGridSort interfaceOne sort term. The elements sort by an ordered list of these, so single-column sort is a list of one and no second code path exists for it.
keystring
dirSuiGridSortDir
SuiGridSortDir type
No members.
SuiGridValidator typeReturns null when the value is acceptable, or the message to show when it is not.
No members.
SuiGridValueType typeHow a column's values are compared, formatted and filtered when the column does not say otherwise. text is the fallback for anything unrecognised.
No members.
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

Color27

--accentthe one chart highlight: the flagged series, down candles, the peak cell--accent-inkorange shaded for small text on light--accent-markthe accent drawn as a mark on a page: active bars, sort arrows, the logotype--accent-softorange 18% on white, warning fills, highlights--accent-soft-ink--bordercard + table outer borders--border-strongcontrol borders, emphasized rules--brand--brand-contrasttext and glyphs sitting on solid brand fills--brand-softnavy 10% on white, quiet chips, selected rows--brand-soft-ink--control-borderthe resting edge of every field and control: 3:1 on its grounds--danger--disabled-bg--focus-halo--focus-ring--hairlinerow separators--ink--ink-faintmetadata beside body ink: 4.8:1 on the page grounds (5.4:1 in dark)--ink-mutedsecondary text: 7:1 on the page grounds and 1.5 times faint--ink-muted-on-washthe muted ink a selected, current or pressed row paints: 4.5:1 on that wash--ink-placeholdera field's placeholder, not a text tier: 4.5:1 on the page grounds--surfacecards, tables, inputs--surface-hovera hovered row or quiet control on any ground: the ink, translucent--surface-raisedmenus, popovers, toasts: the card in light, a step above it in dark--surface-selecteda selected row or option: the primary's hue, always heavier than a hover--surface-sunkentable headers, wells

Typography6

Ag--font-sans1.35--leading-snug•--numeric-tabularfigures that line up in columns: the bundled DM Sans carries tabular figures13.5--text-mdDEFAULT, table cells, controls, body in-app11.5--text-xspills, column headers (11px), captions0.06--tracking-wideuppercase column headers

Spacing and size6

28--control-sm8--space-216--table-cell-x11--table-cell-y8--table-cell-y-compact700--z-dropdown

Radius5

--radius-lgcards, tables, panels, dialogs--radius-mdALL controls: inputs, buttons, selects--radius-pillstatus pills, tabs-as-pills, company chip--radius-smlegend keys, small chips--radius-xscheckboxes, mini swatches

Elevation2

--focus-shadow--shadow-popdropdowns, popovers, toasts

Motion3

--duration-fasthovers, presses, every exit--duration-overlayevery entrance: palette, popover, dialog, drawer, toast, menu, select list--ease-out

Icon1

•--icon-colordefault UI icon ink

Every name above is listed with its value and its role in Foundations.

See also