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.
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.
// 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.
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.
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>
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.
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>
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.
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.
// 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.
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>
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.
// 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.
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.
Moves 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 keys
Move 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.
EnterSpace on a header
Sorts a sortable column, cycling ascending, descending and off; on the select-all header, ticks or clears every row.
Tab in the filter row
Moves 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 header
Opens 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.
HomeEnd
Move 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.
Space
Ticks 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+HomeCtrl+End
Move to the first cell of the grid, or the last.
PageUpPageDown
Move a viewport of rows at a time.
Shift+Arrow keys
Extend the selected range from the anchor, when rangeSelection is on; a pointer drag from a cell selects the same rectangle.
Ctrl+A
Selects every cell, when rangeSelection is on.
Ctrl+C
Copies 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.
Enter
Opens the editor on an editable cell, and otherwise emits suiCellActivate. Inside an editor it commits and steps down a row.
F2
Opens the editor with the current value selected, when edit-mode is cell.
Any printable key
Opens the editor and becomes its first character, when edit-mode is cell.
Escape
Abandons 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+V
Parses the clipboard as a table and emits suiPaste over the focused cell.
Ctrl+Shift+CCtrl+X
Copy with the column headings above, or copy and emit suiCut for the host to blank the cells, when edit-mode is cell.
Ctrl+ZCtrl+Y
Undo and redo the last edit, cut, paste or fill, a whole block at a time; Ctrl+Shift+Z also redoes.
Shift+F10Menu key
Emits suiCellContextMenu for the focused cell, as a right click does.
API
<sui-data-grid>
Properties
Property
Attribute
Type
Default
Description
columnLayout
property only
SuiGridLayout
{}
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.
columns
property only
SuiGridColumn[]
[]
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.
copyFormulas
copy-formulas
boolean
false
Copies 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.
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.
emptyFilterMessage
empty-filter-message
stringstring | undefined
Text 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.
emptyMessage
empty-message
stringstring | undefined
Text 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.
filterDisplay
filter-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.
filterModel
property only
SuiGridFilterModelSuiGridFilterModel | undefined
The 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.
focusedCell
property only
SuiGridCellSuiGridCell | undefined
The focused cell. Mutable: the arrow keys and a click write it before suiCellFocus fires. Property, not attribute.
footerTotals
property only
string | undefined
A 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.
hideSelectionBar
hide-selection-bar
boolean
false
Leaves 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.
loading
loading
boolean
false
Renders 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.
nullOrder
null-order
SuiGridNullOrder"first" | "last"
'last'
Where blanks land, in both directions. Mutable: a grid with stateKey restores it with the sort.
overscan
overscan
number
6
Rows 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.
pinnedBottom
property only
SuiGridRow[]SuiGridRow[] | undefined
The same, below the scrolling rows. Property, not attribute.
pinnedTop
property only
SuiGridRow[]SuiGridRow[] | undefined
Rows 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.
rangeSelection
range-selection
boolean
false
Lets 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.
reorderableColumns
reorderable-columns
boolean
false
Lets a header be dragged onto another to reorder the columns. The new order lands in columnLayout and suiColumnLayoutChange fires with it.
Extra 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.
rowHeights
property only
Map<number, number> | undefined
Heights for individual rows, by row index, when they differ from the density. Sparse: the uniform case costs nothing. Property, not attribute.
Row 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.
rowNumbers
row-numbers
boolean
false
Draws 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.
rows
property only
SuiGridRow[]
[]
Row records, read by column key. Property, not attribute.
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.
selectionMode
selection-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.
showColumnToggle
show-column-toggle
boolean
false
Shows 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.
sortIndicator
sort-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.
sortMode
sort-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.
sorts
property only
SuiGridSort[]
[]
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.
stateKey
state-key
stringstring | undefined
Storage 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.
stateStorage
state-storage
"local" | "session"
'session'
Where stateKey writes: this tab only, or across tabs and restarts.
Events
Event
Detail
React
Vue
Description
suiCellActivate
SuiGridCell
onSuiCellActivate
@sui-cell-activate
Fires when a cell is activated with Enter or a double click; detail is { row, col }.
suiCellContextMenu
SuiGridContextMenuDetail
onSuiCellContextMenu
@sui-cell-context-menu
Fires 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.
suiCellEdit
SuiGridCellEditDetail
onSuiCellEdit
@sui-cell-edit
Fires 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.
suiCellFocus
SuiGridCell
onSuiCellFocus
@sui-cell-focus
Fires when the focused cell moves, by key or by pointer; detail is { row, col }. Does not bubble.
suiColumnLayoutChange
SuiGridLayout
onSuiColumnLayoutChange
@sui-column-layout-change
Fires after a column resize, reorder or toggle; detail is the whole columnLayout, ready to persist. Does not bubble.
suiCopy
string
onSuiCopy
@sui-copy
Fires 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.
suiCopyError
string
onSuiCopyError
@sui-copy-error
Fires 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.
suiCut
SuiGridFillDetail
onSuiCut
@sui-cut
Fires 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.
suiFill
SuiGridFillDetail
onSuiFill
@sui-fill
Fires when the fill handle is released; detail carries the rectangle to write and the values the series inference produced for it.
suiFilter
SuiGridFilterModel
onSuiFilter
@sui-filter
Fires after the filter row or the filter menu rewrites a rule; detail is the whole model. Does not bubble.
suiPaste
SuiGridPasteDetail
onSuiPaste
@sui-paste
Fires 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.
suiRangeChange
number
onSuiRangeChange
@sui-range-change
Fires when the selected range changes; detail is the number of cells now selected. Does not bubble.
Fires 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.
suiSort
SuiGridSort[]
onSuiSort
@sui-sort
Fires when a header click changes the sort; detail is the new ordered term list. Does not bubble.
suiViewport
SuiGridViewportDetail
onSuiViewport
@sui-viewport
Fires 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.
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.
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.
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
Slot
Description
empty
Shown in place of the rows when there are none.
header
Controls 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
SuiGridAligntypeCell 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.
SuiGridCellinterfaceOne cell, by position in the visible order.
row
number
col
number
SuiGridCellEditDetailinterfaceOne 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 rowssourceIndex, 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.
data
SuiGridRow
The record, as the element read it.
rowKey
SuiKey
rowKey(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.
sourceIndex
number
The record's index in rows, or -1 when it is no longer there.
rowIndex
number
The 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.
columnKey
string
The edited column's key.
value
string
The 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.
previous
string
The 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.
SuiGridCellRangeinterfaceA rectangle of cells, inclusive at both ends and normalized so r1 <= r2 and c1 <= c2.
r1
number
c1
number
r2
number
c2
number
SuiGridCellValuetypeWhat 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.
SuiGridColumninterfaceOne 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).
key
string
Unique identity, and the row field read unless field or value says otherwise.
header
string
Column heading.
headerSr?
string
Heading read by assistive technology when header is empty: an actions column shows no words and still names itself. Ignored when header has text.
field?
string
Row field to read, when it is not key.
value?
(row: SuiGridRow) => unknown
Computed 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.
Cell 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?
SuiGridValueType
Drives the default comparator, the default filter match modes and the default alignment.
align?
SuiGridAlign
A 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?
boolean
Aligned 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?
boolean
One line, clipped with an ellipsis, and the full value on the cell's title.
width?
number
Starting width in px. Omit to let flex share the space.
minWidth?
number
maxWidth?
number
flex?
number
Share of the space left after fixed widths. flex: 1 on two columns splits it evenly.
sortable?
boolean
resizable?
boolean
Draggable edge on the header. Off by default; a resized column drops its flex.
hidden?
boolean
Excluded 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?
SuiGridPin
Pins the column to an edge. Pinned columns are never windowed away.
compare?
(a: unknown, b: unknown) => number
Custom comparator; overrides type. Return negative, zero or positive.
format?
(value: unknown, row: SuiGridRow) => string
Formats 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?
boolean
Drawn in --ink-muted: a memo, an account type, anything secondary.
headerTitle?
string
Header tooltip, for a column whose heading had to be short.
editable?
boolean
Opts 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.
Extra 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?
boolean
Offers this column a filter control, in the filter row or the filter menu. Ignored while the element's filterDisplay is none.
matchMode?
SuiGridMatchMode
Match mode the column's filter opens with. Defaults by type: contains for text, equals for numbers and booleans, dateIs for dates.
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?
SuiGridEditor
The 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?
SuiGridValidator
Checked when an edit commits. Returning a message refuses the commit and keeps the editor open with the message beside it.
SuiGridContextMenuDetailinterfaceWhere 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.
row
number
col
number
key
string
The column's key, so a handler does not have to index the model to find out what was hit.
clientX
number
clientY
number
data
SuiGridRow
The record as the grid read it.
sourceIndex
number
Its index in rows, or -1 when it is no longer there.
rowKey?
SuiKey
rowKey(data), present when the grid has a rowKey.
SuiGridDensitytypeRow 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.
SuiGridEditorinterfaceA 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) => HTMLElement
Builds the control, populated and ready. The grid mounts it in the cell and focuses it.
read
(el: HTMLElement) => string
The text to commit, read from the element mount returned.
handlesKey?
(e: KeyboardEvent, el: HTMLElement) => boolean
Optional: keys this editor consumes itself, so the grid does not also act on them.
SuiGridEditorContextinterfaceWhat an editor is given when it opens.
value
unknown
The raw value from the row.
text
string
What the cell was showing, which is what a text editor seeds with.
column
SuiGridColumn
row
SuiGridRow
seed?
string
Seed text when the editor was opened by a printable keystroke, else undefined.
SuiGridExcludeSelectioninterfaceEverything is selected except these keys.
mode
'exclude'
keys
ReadonlySet<SuiKey>
SuiGridFillDetailinterfaceA 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.
source
SuiGridCellRange
The rectangle the drag started from.
target
SuiGridCellRange
The 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.
values
string[][]
Row-major text for target, from the pattern in source.
keys
string[]
Column key for each column of the rectangle, left to right.
data
SuiGridRow[]
The record for each row of the rectangle, top to bottom.
sourceIndexes
number[]
Each of those records' index in rows, or -1.
SuiGridFilterConditioninterfaceOne 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.
key
string
matchMode
SuiGridMatchMode
value?
unknown
SuiGridFilterConstraintinterfaceOne 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
matchMode
SuiGridMatchMode
SuiGridFilterGroupinterfaceConditions 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)[]
SuiGridFilterModelinterfaceThe 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
SuiGridFilterRuleinterfaceEvery 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.
key
string
The 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'
constraints
SuiGridFilterConstraint[]
SuiGridGlobalFilterinterfaceOne 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.
value
string
keys
readonly string[]
matchMode?
SuiGridMatchMode
Defaults to contains.
SuiGridIncludeSelectioninterfaceThe keys that are selected.
mode
'include'
keys
ReadonlySet<SuiKey>
SuiGridLayoutinterfaceThe 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.
SuiGridMatchModetypeHow 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.
SuiGridNullOrdertypeWhere 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.
SuiGridPasteDetailinterfaceA 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.
row
number
col
number
target
SuiGridCellRange
The rectangle the block covers, from the focused cell down and to the end.
values
string[][]
Row-major text for target.
keys
string[]
Column key for each column of the rectangle, left to right.
data
SuiGridRow[]
The record for each row of the rectangle, top to bottom.
sourceIndexes
number[]
Each of those records' index in rows, or -1.
SuiGridPintypeWhich 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.
SuiGridRangeRecordsinterfaceThe 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.
keys
string[]
Column key for each column of the rectangle, left to right.
data
SuiGridRow[]
The record for each row of the rectangle, top to bottom.
sourceIndexes
number[]
Each of those records' index in rows, or -1.
SuiGridRecordRefinterfaceThe 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.
data
SuiGridRow
The record as the grid read it.
sourceIndex
number
Its index in rows, or -1 when it is no longer there.
rowKey?
SuiKey
rowKey(data), present when the grid has a rowKey.
SuiGridRowtypeA 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.
SuiGridRowSelectiontypeWhich 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.
SuiGridSortinterfaceOne 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.
key
string
dir
SuiGridSortDir
SuiGridSortDirtype
No members.
SuiGridValidatortypeReturns null when the value is acceptable, or the message to show when it is not.
No members.
SuiGridValueTypetypeHow 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.
SuiGridViewportDetailinterfaceThe 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.
rowStart
number
rowEnd
number
colStart
number
colEnd
number
SuiKeytypeA 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.