Skip to content
ShineUI
Install

DataGrid

A million rows, cell by cell.

<sui-data-grid>DataStable

About

The spreadsheet-scale grid: hundreds of thousands of rows and hundreds of columns, windowed on both axes so the page holds a few dozen cells however large the data is. Focus moves cell by cell under the arrow keys. aria-label or aria-labelledby on the host names it. Use sui-data-table when the data reads as a document.

Usage

<sui-data-grid aria-label="Journal"></sui-data-grid> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-data-grid"); el.columns = [ { key: "ref", header: "Journal #", width: 150, sortable: true, resizable: true }, { key: "cust", header: "Customer", minWidth: 200, flex: 1, sortable: true, resizable: true }, { key: "due", header: "Date", width: 130, type: "date", sortable: true, resizable: true }, { key: "amount", header: "Amount", width: 140, money: true, sortable: true, format: (v) => { /* ... */ }, resizable: true }, { key: "st", header: "Status", width: 130, render: (row) => { /* ... */ } }, ]; /* SuiGridColumn[] */ el.rows = [ { ref: "JRNL-10000", cust: "Silver Hills Church", due: "2026-01-01", amount: 0, st: "Paid" }, { ref: "JRNL-10001", cust: "Riverbend Dental", due: "2026-02-02", amount: 79.19, st: "Open" }, { ref: "JRNL-10002", cust: "Clark County Schools", due: "2026-03-03", amount: 158.38, st: "Overdue" }, { ref: "JRNL-10003", cust: "Meridian Fitness", due: "2026-04-04", amount: 237.57, st: "Draft" }, { ref: "JRNL-10004", cust: "Northgate Supply", due: "2026-05-05", amount: 316.76, st: "Scheduled" }, { ref: "JRNL-10005", cust: "Halloway and Price", due: "2026-06-06", amount: 395.95, st: "Paid" }, { ref: "JRNL-10006", cust: "Cedar Ridge Farms", due: "2026-07-07", amount: 475.14, st: "Open" }, { ref: "JRNL-10007", cust: "Blue Harbor Marine", due: "2026-08-08", amount: 554.33, st: "Overdue" }, { ref: "JRNL-10008", cust: "Silver Hills Church", due: "2026-09-09", amount: 633.52, st: "Draft" }, { ref: "JRNL-10009", cust: "Riverbend Dental", due: "2026-10-10", amount: 712.71, st: "Scheduled" }, { ref: "JRNL-10010", cust: "Clark County Schools", due: "2026-11-11", amount: 791.9, st: "Paid" }, { ref: "JRNL-10011", cust: "Meridian Fitness", due: "2026-12-12", amount: 871.09, st: "Open" }, { ref: "JRNL-10012", cust: "Northgate Supply", due: "2026-01-13", amount: 950.28, st: "Overdue" }, { ref: "JRNL-10013", cust: "Halloway and Price", due: "2026-02-14", amount: 1029.47, st: "Draft" }, { ref: "JRNL-10014", cust: "Cedar Ridge Farms", due: "2026-03-15", amount: 1108.66, st: "Scheduled" }, /* ... 99985 more */ ]; /* SuiGridRow[] */ el.addEventListener("suiSelectionChange", (e) => console.log(e.detail)); </script>
import { SuiDataGrid } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const columns = [ { key: "ref", header: "Journal #", width: 150, sortable: true, resizable: true }, { key: "cust", header: "Customer", minWidth: 200, flex: 1, sortable: true, resizable: true }, { key: "due", header: "Date", width: 130, type: "date", sortable: true, resizable: true }, { key: "amount", header: "Amount", width: 140, money: true, sortable: true, format: (v) => { /* ... */ }, resizable: true }, { key: "st", header: "Status", width: 130, render: (row) => { /* ... */ } }, ]; /* SuiGridColumn[] */ const rows = [ { ref: "JRNL-10000", cust: "Silver Hills Church", due: "2026-01-01", amount: 0, st: "Paid" }, { ref: "JRNL-10001", cust: "Riverbend Dental", due: "2026-02-02", amount: 79.19, st: "Open" }, { ref: "JRNL-10002", cust: "Clark County Schools", due: "2026-03-03", amount: 158.38, st: "Overdue" }, { ref: "JRNL-10003", cust: "Meridian Fitness", due: "2026-04-04", amount: 237.57, st: "Draft" }, { ref: "JRNL-10004", cust: "Northgate Supply", due: "2026-05-05", amount: 316.76, st: "Scheduled" }, { ref: "JRNL-10005", cust: "Halloway and Price", due: "2026-06-06", amount: 395.95, st: "Paid" }, { ref: "JRNL-10006", cust: "Cedar Ridge Farms", due: "2026-07-07", amount: 475.14, st: "Open" }, { ref: "JRNL-10007", cust: "Blue Harbor Marine", due: "2026-08-08", amount: 554.33, st: "Overdue" }, { ref: "JRNL-10008", cust: "Silver Hills Church", due: "2026-09-09", amount: 633.52, st: "Draft" }, { ref: "JRNL-10009", cust: "Riverbend Dental", due: "2026-10-10", amount: 712.71, st: "Scheduled" }, { ref: "JRNL-10010", cust: "Clark County Schools", due: "2026-11-11", amount: 791.9, st: "Paid" }, { ref: "JRNL-10011", cust: "Meridian Fitness", due: "2026-12-12", amount: 871.09, st: "Open" }, { ref: "JRNL-10012", cust: "Northgate Supply", due: "2026-01-13", amount: 950.28, st: "Overdue" }, { ref: "JRNL-10013", cust: "Halloway and Price", due: "2026-02-14", amount: 1029.47, st: "Draft" }, { ref: "JRNL-10014", cust: "Cedar Ridge Farms", due: "2026-03-15", amount: 1108.66, st: "Scheduled" }, /* ... 99985 more */ ]; /* SuiGridRow[] */ return <SuiDataGrid aria-label="Journal" columns={columns} rows={rows} onSuiSelectionChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiDataGrid } from "@shineui/vue"; import "@shineui/core/shineui.css"; const columns = [ { key: "ref", header: "Journal #", width: 150, sortable: true, resizable: true }, { key: "cust", header: "Customer", minWidth: 200, flex: 1, sortable: true, resizable: true }, { key: "due", header: "Date", width: 130, type: "date", sortable: true, resizable: true }, { key: "amount", header: "Amount", width: 140, money: true, sortable: true, format: (v) => { /* ... */ }, resizable: true }, { key: "st", header: "Status", width: 130, render: (row) => { /* ... */ } }, ]; // SuiGridColumn[] const rows = [ { ref: "JRNL-10000", cust: "Silver Hills Church", due: "2026-01-01", amount: 0, st: "Paid" }, { ref: "JRNL-10001", cust: "Riverbend Dental", due: "2026-02-02", amount: 79.19, st: "Open" }, { ref: "JRNL-10002", cust: "Clark County Schools", due: "2026-03-03", amount: 158.38, st: "Overdue" }, { ref: "JRNL-10003", cust: "Meridian Fitness", due: "2026-04-04", amount: 237.57, st: "Draft" }, { ref: "JRNL-10004", cust: "Northgate Supply", due: "2026-05-05", amount: 316.76, st: "Scheduled" }, { ref: "JRNL-10005", cust: "Halloway and Price", due: "2026-06-06", amount: 395.95, st: "Paid" }, { ref: "JRNL-10006", cust: "Cedar Ridge Farms", due: "2026-07-07", amount: 475.14, st: "Open" }, { ref: "JRNL-10007", cust: "Blue Harbor Marine", due: "2026-08-08", amount: 554.33, st: "Overdue" }, { ref: "JRNL-10008", cust: "Silver Hills Church", due: "2026-09-09", amount: 633.52, st: "Draft" }, { ref: "JRNL-10009", cust: "Riverbend Dental", due: "2026-10-10", amount: 712.71, st: "Scheduled" }, { ref: "JRNL-10010", cust: "Clark County Schools", due: "2026-11-11", amount: 791.9, st: "Paid" }, { ref: "JRNL-10011", cust: "Meridian Fitness", due: "2026-12-12", amount: 871.09, st: "Open" }, { ref: "JRNL-10012", cust: "Northgate Supply", due: "2026-01-13", amount: 950.28, st: "Overdue" }, { ref: "JRNL-10013", cust: "Halloway and Price", due: "2026-02-14", amount: 1029.47, st: "Draft" }, { ref: "JRNL-10014", cust: "Cedar Ridge Farms", due: "2026-03-15", amount: 1108.66, st: "Scheduled" }, /* ... 99985 more */ ]; // SuiGridRow[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiDataGrid aria-label="Journal" :columns="columns" :rows="rows" @sui-selection-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiDataGrid } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiDataGrid], template: `<sui-data-grid aria-label="Journal" [(selection)]="selection" [columns]="columns" [rows]="rows"></sui-data-grid>`, }) export class Example { selection?: SuiGridExcludeSelection | SuiGridIncludeSelection; columns = [ { key: "ref", header: "Journal #", width: 150, sortable: true, resizable: true }, { key: "cust", header: "Customer", minWidth: 200, flex: 1, sortable: true, resizable: true }, { key: "due", header: "Date", width: 130, type: "date", sortable: true, resizable: true }, { key: "amount", header: "Amount", width: 140, money: true, sortable: true, format: (v) => { /* ... */ }, resizable: true }, { key: "st", header: "Status", width: 130, render: (row) => { /* ... */ } }, ]; // SuiGridColumn[] rows = [ { ref: "JRNL-10000", cust: "Silver Hills Church", due: "2026-01-01", amount: 0, st: "Paid" }, { ref: "JRNL-10001", cust: "Riverbend Dental", due: "2026-02-02", amount: 79.19, st: "Open" }, { ref: "JRNL-10002", cust: "Clark County Schools", due: "2026-03-03", amount: 158.38, st: "Overdue" }, { ref: "JRNL-10003", cust: "Meridian Fitness", due: "2026-04-04", amount: 237.57, st: "Draft" }, { ref: "JRNL-10004", cust: "Northgate Supply", due: "2026-05-05", amount: 316.76, st: "Scheduled" }, { ref: "JRNL-10005", cust: "Halloway and Price", due: "2026-06-06", amount: 395.95, st: "Paid" }, { ref: "JRNL-10006", cust: "Cedar Ridge Farms", due: "2026-07-07", amount: 475.14, st: "Open" }, { ref: "JRNL-10007", cust: "Blue Harbor Marine", due: "2026-08-08", amount: 554.33, st: "Overdue" }, { ref: "JRNL-10008", cust: "Silver Hills Church", due: "2026-09-09", amount: 633.52, st: "Draft" }, { ref: "JRNL-10009", cust: "Riverbend Dental", due: "2026-10-10", amount: 712.71, st: "Scheduled" }, { ref: "JRNL-10010", cust: "Clark County Schools", due: "2026-11-11", amount: 791.9, st: "Paid" }, { ref: "JRNL-10011", cust: "Meridian Fitness", due: "2026-12-12", amount: 871.09, st: "Open" }, { ref: "JRNL-10012", cust: "Northgate Supply", due: "2026-01-13", amount: 950.28, st: "Overdue" }, { ref: "JRNL-10013", cust: "Halloway and Price", due: "2026-02-14", amount: 1029.47, st: "Draft" }, { ref: "JRNL-10014", cust: "Cedar Ridge Farms", due: "2026-03-15", amount: 1108.66, st: "Scheduled" }, /* ... 99985 more */ ]; // SuiGridRow[] }

Basic

columns and rows are properties, and the host needs a height because the grid fills what it is given. One hundred thousand rows, of which the page holds about sixty: the window follows the scroll and leans into the direction of travel. A resizable column has a grip on its header edge. aria-label names the grid.

<div style="height: 420px"><sui-data-grid aria-label="Journal"></sui-data-grid></div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); g.columns = base(root.ownerDocument); g.rows = ledger(100000); }

Sorting

A sortable header cycles ascending, descending, then off. With sort-mode="multiple", Shift and a click add a second term and the badge shows its place. By default only the sorted columns draw their arrows and the rest show theirs on hover; sort-indicator="all", as here, draws every glyph at rest.

<div style="height: 380px"><sui-data-grid sort-mode="multiple" sort-indicator="all"></sui-data-grid></div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); g.columns = base(root.ownerDocument); g.rows = ledger(100000); g.sorts = [{ key: 'cust', dir: 'asc' }, { key: 'amount', dir: 'desc' }]; }

Pinned columns and forty columns

A column with pin="start" or pin="end" is held against that edge by the compositor, never by a scroll handler, so it cannot lag the body by a frame. Only the scrolling columns are windowed; pinned ones are always mounted.

<div style="height: 380px"><sui-data-grid density="compact"></sui-data-grid></div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); const cols = wide(root.ownerDocument); cols[0].pin = 'start'; cols[1].pin = 'start'; cols[cols.length - 1].pin = 'end'; g.columns = cols; g.rows = wideRows(100000); }

Filtering

A column with filter: true takes part. Row puts a box under each header, with a button on its right that picks the match mode. Menu puts a funnel on the header edge: as many conditions as you need, joined by And or Or, or values to tick when the column sets filterOptions. Both emit suiFilter.

Type in a box, or open a funnel.
<div style="display:grid;gap:10px"> <div style="display:flex;align-items:center;justify-content:space-between;gap:12px"> <code id="dg-filter-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Type in a box, or open a funnel.</code> <sui-select-button id="dg-filter-mode" aria-label="Filter display" size="sm" value="row"></sui-select-button> </div> <div style="height: 360px; min-width: 0"><sui-data-grid id="dg-filter" filter-display="row" empty-filter-message="No journals match."></sui-data-grid></div> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#dg-filter'); const log = root.querySelector('#dg-filter-log'); const mode = root.querySelector('#dg-filter-mode'); mode.options = [{ label: 'Row', value: 'row' }, { label: 'Menu', value: 'menu' }]; mode.addEventListener('suiChange', (e) => { g.filterDisplay = e.detail; }); g.columns = base(root.ownerDocument).map((c) => ({ ...c, filter: true, ...(c.key === 'st' ? { filterOptions: STATUS } : {}) })); g.rows = ledger(100000); const said = (c) => (c.value === undefined ? c.matchMode : `${c.matchMode} ${Array.isArray(c.value) ? c.value.join(' | ') : c.value}`); g.addEventListener('suiFilter', (e) => { const rules = Object.entries(e.detail.rules || {}); log.textContent = rules.length ? rules.map(([k, r]) => `${k} ${r.constraints.map(said).join(` ${r.operator} `)}`).join(', ') : 'No filter.'; }); }

An advanced filter

For conditions across columns, joined by And or Or and nested in groups, put a sui-filter-builder in the header slot. It sits beside the Columns button, counts what is applied, and writes the grid filter model on Apply through setFilter.

<div style="height: 360px; min-width: 0"> <sui-data-grid id="dg-advanced" show-column-toggle> <sui-filter-builder slot="header" size="sm"></sui-filter-builder> </sui-data-grid> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#dg-advanced'); g.columns = base(root.ownerDocument).map((c) => ({ ...c, filter: true, ...(c.key === 'st' ? { filterOptions: STATUS } : {}) })); g.rows = ledger(100000); g.setFilter({ tree: { operator: 'or', items: [{ key: 'st', matchMode: 'in', value: ['Overdue'] }, { key: 'amount', matchMode: 'gt', value: 8900 }] } }); }

Reorder and hide columns

reorderable-columns lets a header be dragged onto another, and show-column-toggle adds a Columns button that hides one. Both write the whole arrangement into columnLayout and emit suiColumnLayoutChange, which is the shape to store and hand back.

Drag a header onto another, or open Columns.
<div style="display:grid;gap:10px"> <code id="dg-layout-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Drag a header onto another, or open Columns.</code> <div style="height: 340px; min-width: 0"><sui-data-grid id="dg-layout" reorderable-columns show-column-toggle></sui-data-grid></div> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#dg-layout'); const log = root.querySelector('#dg-layout-log'); g.columns = base(root.ownerDocument); g.rows = ledger(5000); g.addEventListener('suiColumnLayoutChange', (e) => { const order = (e.detail.order || []).join(' '); const hidden = (e.detail.hidden || []).join(' '); log.textContent = `order: ${order || 'declared'}${hidden ? `, hidden: ${hidden}` : ''}`; }); }

Range selection and copy

range-selection lets a pointer drag, a Shift+click and Shift with the arrows select a rectangle, and Ctrl or Cmd with the pointer add a second one. Ctrl+C copies it as tab-separated text, which pastes into a spreadsheet as cells.

Nothing selected.
<div style="display: flex; flex-direction: column; gap: 10px; height: 400px"> <div style="font-size: 12.5px; color: var(--ink-muted)" id="dg-count">Nothing selected.</div> <div style="flex: 1; min-height: 0"><sui-data-grid range-selection></sui-data-grid></div> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); const out = root.querySelector('#dg-count'); g.columns = base(root.ownerDocument); g.rows = ledger(50000); g.addEventListener('suiRangeChange', (e) => { out.textContent = e.detail === 0 ? 'Nothing selected.' : `${e.detail.toLocaleString('en-US')} cells selected. Press Ctrl+C to copy.`; }); }

Row numbers

row-numbers adds a numbered column on the leading edge that counts the rows as shown, so a sort or a filter renumbers it. It is never sorted, edited, copied or focused. With range-selection a click on a number selects the row, Shift extends to it, and the corner selects every cell.

<div style="height: 360px; min-width: 0"><sui-data-grid row-numbers range-selection></sui-data-grid></div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); g.columns = base(root.ownerDocument); g.rows = ledger(100000); }

A million rows

Past the height a browser will let an element be, the grid compresses the scroll space and maps between the two. The scrollbar becomes an approximate control at this scale; scrollToRow is how you address a row exactly.

<div style="display: flex; flex-direction: column; gap: 10px; height: 400px"> <div style="display: flex; gap: 8px; align-items: flex-end"> <sui-input-number id="dg-row" label="Row" value="750000" style="width: 160px"></sui-input-number> <sui-button id="dg-go" variant="outline" label="Go to row"></sui-button> </div> <div style="flex: 1; min-height: 0"><sui-data-grid density="compact"></sui-data-grid></div> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); g.columns = base(root.ownerDocument); g.rows = ledger(1000000); root.querySelector('#dg-go').addEventListener('click', () => { const n = Number(root.querySelector('#dg-row').value) || 0; g.scrollToRow(n, 'start'); }); }

Editing

edit-mode="cell" turns on in-cell editing. The grid reports the edit and does not write the row: apply suiCellEdit through its sourceIndex, the record in rows, or its rowKey, because rowIndex is a position in the sorted view. A column opts out with editable: false; one that draws its own cell is never editable.

Press F2, or just start typing, on a cell.
<div style="display: flex; flex-direction: column; gap: 10px; height: 400px"> <div style="font-size: 12.5px; color: var(--ink-muted)" id="dg-edit">Press F2, or just start typing, on a cell.</div> <div style="flex: 1; min-height: 0"><sui-data-grid edit-mode="cell"></sui-data-grid></div> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); const out = root.querySelector('#dg-edit'); const cols = base(root.ownerDocument); cols[0].editable = false; cols[2].editor = dateEditor(root.ownerDocument); cols[3].editor = numberEditor(root.ownerDocument, 0.01); g.columns = cols; g.rows = ledger(20000); g.addEventListener('suiCellEdit', (e) => { const { sourceIndex, data, columnKey, value, previous } = e.detail; const next = g.rows.slice(); next[sourceIndex] = { ...next[sourceIndex], [columnKey]: columnKey === 'amount' ? Number(String(value).replace(/[^0-9.-]/g, '')) || 0 : value }; g.rows = next; out.textContent = `${data.ref}, ${columnKey}: "${previous}" became "${value}".`; }); }

The fill handle

edit-mode="cell" or range-selection puts a small square on the corner of the selection. Drag it across rows, columns or both. The grid reads the source for a pattern: a numeric step, a date step, or a counter like JRNL-1041. suiFill carries the values with their records and columns, and the host writes them.

Select a cell or two, then drag the square on the corner.
<div style="display: flex; flex-direction: column; gap: 10px; height: 400px"> <div style="font-size: 12.5px; color: var(--ink-muted)" id="dg-fill">Select a cell or two, then drag the square on the corner.</div> <div style="flex: 1; min-height: 0"><sui-data-grid range-selection></sui-data-grid></div> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); const out = root.querySelector('#dg-fill'); g.columns = base(root.ownerDocument); g.rows = ledger(20000); g.addEventListener('suiFill', (e) => { // sourceIndexes and keys name the records and columns under the fill, so a sort or a // column reorder cannot send a value to the wrong place. const { values, keys, sourceIndexes } = e.detail; const next = g.rows.slice(); sourceIndexes.forEach((at, r) => { const row = { ...next[at] }; keys.forEach((key, c) => { const text = values[r][c]; row[key] = key === 'amount' ? Number(String(text).replace(/[^0-9.-]/g, '')) || 0 : text; }); next[at] = row; }); g.rows = next; out.textContent = `Filled ${sourceIndexes.length * keys.length} cells from the pattern; Ctrl+Z takes them back.`; }); g.addEventListener('suiCellEdit', (e) => { // An undo of the fill arrives as one edit per cell, with source: undo. const { sourceIndex, columnKey, value } = e.detail; const next = g.rows.slice(); next[sourceIndex] = { ...next[sourceIndex], [columnKey]: columnKey === 'amount' ? Number(String(value).replace(/[^0-9.-]/g, '')) || 0 : value }; g.rows = next; }); }

Loading

loading draws shimmer rows in place of the data and marks the grid aria-busy, with a status line a reader hears; the empty band stays away until it clears. The same word every data surface uses.

<div style="height: 200px; min-width: 0"><sui-data-grid id="dg-loading" loading></sui-data-grid></div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#dg-loading'); g.columns = base(root.ownerDocument); g.rows = []; }

Pinned rows and a totals line

pinned-top and pinned-bottom hold rows outside the window, and footer-totals draws a summary keyed by column. None of them take focus or join a range: they are chrome that happens to look like a row, so an arrow key can never walk into a total.

<div style="height: 340px"><sui-data-grid id="dg-pinned"></sui-data-grid></div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#dg-pinned'); const rows = ledger(2000); g.columns = base(root.ownerDocument); g.rows = rows; g.pinnedTop = [{ ref: 'B/FWD', cust: 'Brought forward', amount: 0, due: '', st: '' }]; const sum = rows.reduce((t, r) => t + (Number(r.amount) || 0), 0); g.footerTotals = { cust: `${rows.length.toLocaleString('en-US')} entries`, amount: sum.toLocaleString('en-US') }; }

Undo, cut and a context menu

The grid reports and never writes. An edit, a cut and a paste each name the records they touch; the host writes them, and Ctrl+Z emits each change the other way round with source: undo. A right click reports the cell and the point, and the host opens a sui-context-menu there.

Edit a cell, or cut and paste a block, then press Ctrl+Z. Right click a cell for the menu.
<div style="display:grid;gap:10px"> <code id="dg-undo-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Edit a cell, or cut and paste a block, then press Ctrl+Z. Right click a cell for the menu.</code> <div style="height: 320px; min-width: 0"><sui-data-grid id="dg-undo" edit-mode="cell" range-selection></sui-data-grid></div> <sui-context-menu id="dg-undo-menu" width="260"></sui-context-menu> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#dg-undo'); const log = root.querySelector('#dg-undo-log'); const menu = root.querySelector('#dg-undo-menu'); g.columns = base(root.ownerDocument); g.rows = ledger(2000); // Cells arrive as text; the amount column stores a number. const stored = (key, text) => (key === 'amount' ? (text === '' ? null : Number(String(text).replace(/[^0-9.-]/g, '')) || 0) : text); // Writes [index in rows, column key, text] triples in one new array. Status draws its own cell, so it keeps its value. const write = (changes) => { const next = g.rows.slice(); for (const [at, key, text] of changes) if (at >= 0 && key !== 'st') next[at] = { ...next[at], [key]: stored(key, text) }; g.rows = next; }; const block = (d) => d.sourceIndexes.flatMap((at, r) => d.keys.map((key, c) => [at, key, d.values[r]?.[c] ?? ''])); g.addEventListener('suiCellEdit', (e) => { const d = e.detail; write([[d.sourceIndex, d.columnKey, d.value]]); log.textContent = `${d.source || 'edit'}: ${d.columnKey} on ${d.columnKey === 'ref' ? d.value || 'a cut row' : d.data.ref} is now ${d.value || 'blank'}`; }); g.addEventListener('suiCut', (e) => { write(block(e.detail)); const n = e.detail.data.length * e.detail.keys.length; log.textContent = n === 1 ? `cut ${e.detail.keys[0]} on ${e.detail.data[0].ref} to the clipboard; Ctrl+Z puts it back` : `cut ${n} cells to the clipboard; Ctrl+Z puts them back`; }); g.addEventListener('suiPaste', (e) => { write(block(e.detail)); log.textContent = `pasted ${e.detail.values.length} rows from ${e.detail.data[0]?.ref}`; }); g.addEventListener('suiCopy', (e) => { const lines = e.detail.split('\r\n').length; log.textContent = `copied ${lines} ${lines === 1 ? 'row' : 'rows'} of text to the clipboard`; }); g.addEventListener('suiCopyError', () => { log.textContent = 'the clipboard refused the copy'; }); menu.items = [ { label: 'Cut', shortcut: 'Ctrl+X', command: () => g.cut() }, { label: 'Copy', shortcut: 'Ctrl+C', command: () => g.copy() }, { label: 'Copy with headings', shortcut: 'Ctrl+Shift+C', command: () => g.copy(true) }, { label: 'Paste', shortcut: 'Ctrl+V', command: () => g.paste() }, { separator: true }, { label: 'Undo', shortcut: 'Ctrl+Z', command: () => g.undo() }, { label: 'Redo', shortcut: 'Ctrl+Y', command: () => g.redo() }, ]; g.addEventListener('suiCellContextMenu', (e) => { const at = menu.getBoundingClientRect(); menu.x = e.detail.clientX - at.left; menu.y = e.detail.clientY - at.top; menu.open = true; }); }

Events

suiViewport reports the mounted window and is where a server-paged grid fetches. suiCellFocus follows the focused cell, and suiSort carries the ordered term list. These report the grid state and do not bubble, so listen on the grid itself; cell actions such as suiCellActivate and suiCellEdit do bubble.

<div style="display: flex; flex-direction: column; gap: 10px; height: 400px"> <div style="flex: 1; min-height: 0"><sui-data-grid sort-mode="multiple"></sui-data-grid></div> <sui-terminal id="dg-log" style="height: 108px"></sui-terminal> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('sui-data-grid'); const log = root.querySelector('#dg-log'); const say = (t) => log.print(t); g.columns = base(root.ownerDocument); g.rows = ledger(100000); g.addEventListener('suiViewport', (e) => say(`suiViewport rows ${e.detail.rowStart}-${e.detail.rowEnd}`)); g.addEventListener('suiCellFocus', (e) => say(`suiCellFocus row ${e.detail.row}, col ${e.detail.col}`)); g.addEventListener('suiSort', (e) => say(`suiSort ${e.detail.map((t) => `${t.key} ${t.dir}`).join(', ') || 'cleared'}`)); }

Keyboard

KeyAction
TabMoves into the grid, and out of it again. The grid is one tab stop: the focused cell holds it, so Tab never walks a million cells; the funnel buttons of a menu filter display are tab stops after it. Inside an editor it commits and steps across a column, and Shift reverses both this and Enter.
Arrow keysMove the focused cell one row or one column, scrolling the least that shows it whole. ArrowUp from the first row moves into the column headers; ArrowLeft and ArrowRight walk them and ArrowDown returns to the first row.
Enter Space on a headerSorts a sortable column, cycling ascending, descending and off; on the select-all header, ticks or clears every row.
Tab in the filter rowMoves from a filter box to the mode button on its right, then to the next column's box. Enter or Space on the button opens the list of modes; the arrows move through it and Enter picks one.
Alt+ArrowDown on a headerOpens the column filter menu when filter-display is menu. Inside the menu, Enter in a text or number field applies it, and Tab past the last control closes it and carries on from the funnel.
Home EndMove to the first or last column of the row. With selection-mode="checkbox" the column of boxes is the first; the row numbers never take focus.
SpaceTicks or clears the focused row when selection-mode is set; Shift+Space ticks the run from the last row picked. On a row's box, Enter does the same.
Ctrl+Home Ctrl+EndMove to the first cell of the grid, or the last.
PageUp PageDownMove a viewport of rows at a time.
Shift+Arrow keysExtend the selected range from the anchor, when rangeSelection is on; a pointer drag from a cell selects the same rectangle.
Ctrl+ASelects every cell, when rangeSelection is on.
Ctrl+CCopies the selected range, or the focused cell, as tab-separated text a spreadsheet accepts, and emits suiCopy with that text; a refused clipboard emits suiCopyError instead.
EnterOpens the editor on an editable cell, and otherwise emits suiCellActivate. Inside an editor it commits and steps down a row.
F2Opens the editor with the current value selected, when edit-mode is cell.
Any printable keyOpens the editor and becomes its first character, when edit-mode is cell.
EscapeAbandons the open editor and leaves the value alone, and closes the columns or filter menu without applying it, from inside it or from its button. Inside the menu, an open list or calendar closes first.
Ctrl+VParses the clipboard as a table and emits suiPaste over the focused cell.
Ctrl+Shift+C Ctrl+XCopy with the column headings above, or copy and emit suiCut for the host to blank the cells, when edit-mode is cell.
Ctrl+Z Ctrl+YUndo and redo the last edit, cut, paste or fill, a whole block at a time; Ctrl+Shift+Z also redoes.
Shift+F10 Menu keyEmits suiCellContextMenu for the focused cell, as a right click does.

API

<sui-data-grid>

Properties

PropertyAttributeTypeDefaultDescription
columnLayoutproperty onlySuiGridLayout{}Column order, dragged widths and hidden columns. Mutable: a resize, a reorder and the column toggle write it before suiColumnLayoutChange fires. The same shape sui-data-table takes, so a layout survives a screen moving from one to the other. Property, not attribute.
columnsproperty onlySuiGridColumn[][]Column definitions. key is the identity and the field; width and flex size the column; render draws the cell; pin holds it against an edge. Property, not attribute.
copyFormulascopy-formulasbooleanfalseCopies cells that a spreadsheet would read as a formula (text starting with =, +, -, @, a tab or a line break, other than a plain number) as they are. Off by default: the plain-text copy prefixes such a cell with an apostrophe, the mark Excel and Sheets read as "this is text", so a cell holding =HYPERLINK(…) cannot run when pasted into a spreadsheet, as OWASP advises for CSV and clipboard output. A paste into a ShineUI grid gets the cells exactly either way, from a private clipboard flavour the copy carries beside the plain text.
densitydensitySuiGridDensity"comfortable" | "compact" | "standard"'standard'Row height and cell padding.
editModeedit-mode"cell" | "none"'none'Whether a cell can be edited in place, spelled as on sui-data-table: cell turns editing on and none, the default, leaves the cells read-only. A column opts out with editable: false. F2 or Enter opens the editor, a printable key opens it and replaces the value, Escape abandons it and Enter or Tab commits. The grid emits the edit and does not write the row: the host owns the data.
emptyFilterMessageempty-filter-messagestringstring | undefinedText for a grid whose rows the filter row or the filter menu narrowed to none. 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 grid with no rows at all. An empty string draws the empty band without words, for a host that says it once above the grid. The empty slot replaces both messages.
filterDisplayfilter-display"menu" | "none" | "row"'none'Where a column's filter control lives: a row of boxes under the header, or a menu opened from a funnel on the header's end edge. A column takes part by declaring filter: true. 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. Either way the grid writes the rule into filterModel and emits suiFilter.
filterModelproperty onlySuiGridFilterModelSuiGridFilterModel | undefinedThe filter model: per-column rules and an optional global term. Named for the model, since filter on the list elements is the boolean that shows a search box; the grid's boxes are filterDisplay. Mutable: the filter row and the filter menu rewrite it before suiFilter fires. Property, not attribute.
focusedCellproperty onlySuiGridCellSuiGridCell | undefinedThe focused cell. Mutable: the arrow keys and a click write it before suiCellFocus fires. Property, not attribute.
footerTotalsproperty onlystring | undefinedA summary line under everything, by column key. Text, already formatted, because only the host knows whether a total covers the page, the filtered set or the whole dataset -- and a grid that guessed would be wrong on the one that matters. Property, not attribute.
hideSelectionBarhide-selection-barbooleanfalseLeaves a picked row its tint alone, without the accent bar the grid draws at its start, as sui-list and sui-data-table draw it, so that a selection reads without leaning on the tint.
loadingloadingbooleanfalseRenders shimmer rows in place of the data, marks the grid aria-busy and names the wait in a status line a reader hears; the empty band stays away while it is set. Reflected, so a page can style the wait.
nullOrdernull-orderSuiGridNullOrder"first" | "last"'last'Where blanks land, in both directions. Mutable: a grid with stateKey restores it with the sort.
overscanoverscannumber6Rows kept mounted beyond each edge of the viewport. The window leans into the direction of travel and widens with speed, so this is a floor rather than a fixed count.
pinnedBottomproperty onlySuiGridRow[]SuiGridRow[] | undefinedThe same, below the scrolling rows. Property, not attribute.
pinnedTopproperty onlySuiGridRow[]SuiGridRow[] | undefinedRows held above the scrolling ones, always mounted and never windowed. Use for a running total, a "new row" line or a pinned record a reader is comparing against. They take no part in sorting, filtering, selection or cell focus: they are chrome that happens to look like rows. Property, not attribute.
rangeSelectionrange-selectionbooleanfalseLets the pointer and Shift with the arrow keys select a rectangle of cells, and Ctrl or Cmd with the pointer add a second one. Copying sends the selection to the clipboard as TSV.
reorderableColumnsreorderable-columnsbooleanfalseLets a header be dragged onto another to reorder the columns. The new order lands in columnLayout and suiColumnLayoutChange fires with it.
rowClassproperty only((row: SuiGridRow, index: number) => string | undefined) | undefinedExtra classes for a whole row, by row and index. Called once per mounted row per render, so keep it cheap. Use it for a status tint or an overdue marker; the grid's own row classes are always applied and this adds to them. Property, not attribute.
rowHeightsproperty onlyMap<number, number> | undefinedHeights for individual rows, by row index, when they differ from the density. Sparse: the uniform case costs nothing. Property, not attribute.
rowKeyproperty only((row: SuiGridRow, index: number) => SuiKey) | undefinedRow identity (row, index) => key; defaults to the row index. Identity drives cell caching, so a grid whose rows reorder under it needs a real one. Property, not attribute.
rowNumbersrow-numbersbooleanfalseDraws a numbered column on the leading edge, as a spreadsheet does: each row's place in the rows as shown, counted from 1, so a sort or a filter renumbers it. It is chrome, like the selection column: never sorted, filtered, edited, copied, exported or focused. With rangeSelection a click on a number selects that row's cells, Shift extends to it, and a click on the column's heading selects every cell.
rowsproperty onlySuiGridRow[][]Row records, read by column key. Property, not attribute.
selectionproperty onlySuiGridRowSelectionSuiGridExcludeSelection | SuiGridIncludeSelectionclearSelection()Selected rows, as an include set or an exclude set. The exclude shape is what makes select-all expressible over a dataset whose total the page does not hold. Mutable; written before suiSelectionChange fires. Property, not attribute.
selectionModeselection-mode"checkbox" | "multiple" | "none" | "single"'none'Row selection. checkbox adds a leading column of boxes with a select-all in the header; single and multiple select by clicking a row, with Ctrl or Cmd to add and Shift to extend. Independent of rangeSelection, which selects cells rather than records.
showColumnToggleshow-column-togglebooleanfalseShows a Columns button that opens a checklist of the columns, so a reader can hide the ones they do not want. Hidden columns land in columnLayout.hidden.
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; all draws a glyph on every sortable header.
sortModesort-mode"multiple" | "single"'single'Whether a header click replaces the sort or adds a term. multiple also lets Shift+click add one, and a third click on a column removes 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. Pass a stable array, because the pipeline is memoized on identity and a fresh array on every parent render re-sorts the whole dataset. Property, not attribute.
stateKeystate-keystringstring | undefinedStorage key for the sorts, the filter and the column layout. Leave it unset and the grid remembers nothing. Both data surfaces write the same shape, so a screen that swaps one for the other keeps what its reader arranged.
stateStoragestate-storage"local" | "session"'session'Where stateKey writes: this tab only, or across tabs and restarts.

Events

EventDetailReactVueDescription
suiCellActivateSuiGridCellonSuiCellActivate@sui-cell-activateFires when a cell is activated with Enter or a double click; detail is { row, col }.
suiCellContextMenuSuiGridContextMenuDetailonSuiCellContextMenu@sui-cell-context-menuFires on a right click or the context-menu key over a cell, after focus has moved there. The grid suppresses the browser menu and opens nothing itself: what a menu should contain is the host's business, and sui-context-menu is the element to open with it.
suiCellEditSuiGridCellEditDetailonSuiCellEdit@sui-cell-editFires when an editor commits and the value actually changed, and again for each cell an undo or a redo puts back, with source set. Detail is a SuiGridCellEditDetail, the shape sui-data-table reports too: apply it to your own rows through rowKey or sourceIndex.
suiCellFocusSuiGridCellonSuiCellFocus@sui-cell-focusFires when the focused cell moves, by key or by pointer; detail is { row, col }. Does not bubble.
suiColumnLayoutChangeSuiGridLayoutonSuiColumnLayoutChange@sui-column-layout-changeFires after a column resize, reorder or toggle; detail is the whole columnLayout, ready to persist. Does not bubble.
suiCopystringonSuiCopy@sui-copyFires after a copy or a cut reaches the clipboard; detail is the copied text, the tab-separated rows another application reads, with a formula cell escaped unless copyFormulas is set.
suiCopyErrorstringonSuiCopyError@sui-copy-errorFires when the browser refused a copy or a cut, so nothing reached the clipboard and a cut cleared nothing; detail is the text that was not copied.
suiCutSuiGridFillDetailonSuiCut@sui-cutFires when a cut takes the selection to the clipboard, after its suiCopy. The rectangle is reported and the rows are not written, exactly as with a fill or a paste: the host owns the data.
suiFillSuiGridFillDetailonSuiFill@sui-fillFires when the fill handle is released; detail carries the rectangle to write and the values the series inference produced for it.
suiFilterSuiGridFilterModelonSuiFilter@sui-filterFires after the filter row or the filter menu rewrites a rule; detail is the whole model. Does not bubble.
suiPasteSuiGridPasteDetailonSuiPaste@sui-pasteFires when a block is pasted over the focused cell. The grid parses the clipboard and reports the rectangle; writing it is the host's, since only the host knows whether the dataset may grow.
suiRangeChangenumberonSuiRangeChange@sui-range-changeFires when the selected range changes; detail is the number of cells now selected. Does not bubble.
suiSelectionChangeSuiGridRowSelectionSuiGridExcludeSelection | SuiGridIncludeSelectiononSuiSelectionChange@sui-selection-changeFires when the row 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, so read the mode before treating keys as the selected rows. Does not bubble.
suiSortSuiGridSort[]onSuiSort@sui-sortFires when a header click changes the sort; detail is the new ordered term list. Does not bubble.
suiViewportSuiGridViewportDetailonSuiViewport@sui-viewportFires when the mounted window changes, and once after the first render. Fetch the rows the window asks for here to page a grid from a server. Does not bubble.

Methods

MethodSignatureDescription
autosizeColumn()autosizeColumn(key: string) => Promise<number | null>Widens or narrows a column to fit what is actually in it. Measured from the mounted cells, which is the only honest answer a windowed grid can give: the rows outside the window are not in the document and measuring them would mean mounting a million of them. Every grid that offers this does the same and it is worth saying out loud, because the result is "fits what you can see" rather than "fits the dataset", and a reader who scrolls may find a wider value further down. Each cell is measured at width: max-content for the moment it takes to read its box, then put back. scrollWidth cannot do this job: it never reports less than the element's own width, so a column could only ever widen and a 300px column holding "R-49" fits to 300px. Measuring the cell's children was the next try and it failed in the header, whose label stretches across the cell so the funnel keeps the end edge: the label's box was the column. A max-content box is what the content asks for, padding, glyphs, a pill's own padding and whatever a custom render put inside it included, and it beats a canvas measureText, which would have to be told the font, the letter spacing and the tabular numerals and would still be wrong about a custom render.
copy()copy(withHeaders?: boolean) => Promise<boolean>Copies the selected rectangle, or the focused cell when nothing is selected, as TSV. The plain text escapes a cell a spreadsheet would run as a formula unless copyFormulas is set; a private flavour beside it carries the cells exactly, so a paste into a grid is lossless. Both go in one synchronous copy where the engine allows, else the plain text goes through utils/clipboard.ts, whose fallback covers plain http and a refused Clipboard API. The boolean says whether the text really reached the clipboard, so a caller never confirms a copy that did not happen; suiCopy or suiCopyError says the same to a listener. Ctrl+C calls it, and Ctrl+Shift+C calls it with withHeaders, the column headings as a first row.
cut()cut() => Promise<boolean>Copies the selection and reports the rectangle to clear. The grid does not blank the cells itself, for the same reason it does not write a fill or a paste: only the host knows whether a column may be emptied at all. The clipboard write happens first and the event is only emitted when it succeeded, so a refused clipboard never costs anybody their data. The clipboard holds what copy() would write, formula escapes and the private flavour included. Ctrl+X calls it; it does nothing unless editMode is cell.
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/field and then its format, so the file says what the grid says. Text that a spreadsheet would read as a formula is escaped by default, because the file is opened by someone who did not watch it being made.
focusCell()focusCell(row: number, column: number) => Promise<void>Moves the focused cell and brings it into view.
getViewport()getViewport() => Promise<SuiGridViewportDetail>The mounted window right now, for a caller that needs it outside the event.
paste()paste() => Promise<boolean>Pastes the clipboard over the focused cell and emits suiPaste, for a Paste row in a menu or a toolbar, where there is no keystroke to carry the text. It reads through the Clipboard API, so the browser may ask the person first; it resolves false when that is refused or there is nothing to paste. Ctrl+V does not need it.
redo()redo() => Promise<boolean>Puts back the edit the last undo took away.
scrollToRow()scrollToRow(index: number, align?: "auto" | "start" | "center" | "end") => Promise<void>Scrolls so that row index is in view. align forces a position even when it already is.
setFilter()setFilter(model: SuiGridFilterModel) => Promise<void>Applies a whole filter model the way the filter row and the menus do: it becomes filterModel, is stored under stateKey, and goes out on suiFilter. What sui-filter-builder calls, so a filter applied from outside the grid reaches the same listeners.
setFocus()setFocus() => Promise<void>Focuses the grid where its tab stop is: the focused cell or header when one is in view, else the grid itself, from where the arrow keys start at the first cell in view. el.focus() on the host does the same.
undo()undo() => Promise<boolean>Emits the inverse of the last edit and moves it onto the redo stack. The grid never writes a row, so this reports rather than acts: the host applies the value the same way it applied the edit. Returns false when there is nothing to undo, so a toolbar button can be driven from the answer.

Slots

SlotDescription
emptyShown in place of the rows when there are none.
headerControls in the bar above the grid, before the Columns button: a sui-filter-builder, a search box, a bulk action. The same slot sui-data-table has.

Types

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.
SuiGridCell interfaceOne cell, by position in the visible order.
rownumber
colnumber
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.
SuiGridCellRange interfaceA rectangle of cells, inclusive at both ends and normalized so r1 <= r2 and c1 <= c2.
r1number
c1number
r2number
c2number
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.
SuiGridContextMenuDetail interfaceWhere a context menu was asked for. clientX and clientY are the point to open at, which is what an anchored menu needs and what a keyboard request cannot supply from the pointer.
rownumber
colnumber
keystringThe column's key, so a handler does not have to index the model to find out what was hit.
clientXnumber
clientYnumber
dataSuiGridRowThe record as the grid read it.
sourceIndexnumberIts index in rows, or -1 when it is no longer there.
rowKey?SuiKeyrowKey(data), present when the grid has a rowKey.
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>
SuiGridFillDetail interfaceA fill handle drag, reported once on release. values is what the series inference made of the source, so a host that wants the ordinary answer writes them straight in; source and target are there for one that wants to decide for itself. As with an edit, the grid does not write the rows.
sourceSuiGridCellRangeThe rectangle the drag started from.
targetSuiGridCellRangeThe rectangle to write. A drag along one axis names only the new cells. A drag that grows both axes names the bounds of that extension, and any source cell inside those bounds is repeated unchanged so the rectangle can be written in one pass.
valuesstring[][]Row-major text for target, from the pattern in source.
keysstring[]Column key for each column of the rectangle, left to right.
dataSuiGridRow[]The record for each row of the rectangle, top to bottom.
sourceIndexesnumber[]Each of those records' index in rows, or -1.
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.
SuiGridPasteDetail interfaceA block of cells pasted in, addressed from the focused cell. values is row-major, exactly as the clipboard supplied it, and target is the rectangle it covers from that cell, the same two fields a fill and a cut carry, so one handler writes all three; a handler can clip or grow the dataset as it sees fit.
rownumber
colnumber
targetSuiGridCellRangeThe rectangle the block covers, from the focused cell down and to the end.
valuesstring[][]Row-major text for target.
keysstring[]Column key for each column of the rectangle, left to right.
dataSuiGridRow[]The record for each row of the rectangle, top to bottom.
sourceIndexesnumber[]Each of those records' index in rows, or -1.
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.
SuiGridRangeRecords interfaceThe records and columns a rectangle covers, for a fill, a cut and a paste. Each list runs in the rectangle's order: data[i] and sourceIndexes[i] belong to values[i], and keys[j] is the column of values[i][j]. A rectangle that runs past the last row or column names only the ones that exist, so the lists can be shorter than values.
keysstring[]Column key for each column of the rectangle, left to right.
dataSuiGridRow[]The record for each row of the rectangle, top to bottom.
sourceIndexesnumber[]Each of those records' index in rows, or -1.
SuiGridRecordRef interfaceThe record behind one row of an event. row in a context menu or a paste is a position in the rows as they are shown, sorted and filtered, so it names a different record as soon as the view is reordered. These name the record itself: write through sourceIndex (its index in rows) or rowKey, never through row. A cell edit carries the family's SuiGridCellEditDetail, which spells the position rowIndex.
dataSuiGridRowThe record as the grid read it.
sourceIndexnumberIts index in rows, or -1 when it is no longer there.
rowKey?SuiKeyrowKey(data), present when the grid has a rowKey.
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.
SuiGridViewportDetail interfaceThe window that changed, as suiViewport reports it. Indices are into the current row order and the scrolling columns respectively, end exclusive. Pinned columns are always mounted and never appear in the column window.
rowStartnumber
rowEndnumber
colStartnumber
colEndnumber
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.

Color26

--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-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 size5

28--control-sm8--space-212--space-316--table-cell-x700--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