Theming
Every visual slot is overridable, for one grid or for the whole app, and density drives the geometry through CSS variables rather than through classes.
headerHeight and groupHeaderHeight are new, with the CSS variables they write. Three slots join the table: stickyGroupRows, stickyGroupRow and flashCell. The header group attribute is now part of the DOM contract by name.Per Grid
ui takes a
class per slot. They are merged with the variant's own through tailwind-merge, so a conflicting
utility replaces rather than fights.
<!-- ui overrides one grid, slot by slot. Classes are merged with the
variant's own through tailwind-merge, so a conflicting utility wins
rather than fighting. -->
<DataGrid
{grid}
ui={{
viewport: 'rounded-xl border-primary/30',
headerCell: 'uppercase tracking-wide text-primary',
row: 'even:bg-surface-container-lowest',
cell: 'font-mono'
}}
/>App-Wide Defaults
There is no live demo here on purpose: defineDataGridConfig is global, so calling it on this page would restyle every other demo in the section. The code
is the whole of it.
import { defineDataGridConfig } from '@sv5ui/datagrid';
// App-wide defaults, in the same shape sv5ui components take. Grids read
// this when they mount, so call it once at startup rather than reactively.
defineDataGridConfig({
defaultVariants: { density: 'compact' },
slots: { headerCell: 'uppercase tracking-wide' }
});
// Order of application: variant classes, then this config, then the
// grid's own ui prop.Classes land in three passes, each one merged over the last, so the narrowest wins:
| Pass | Where it comes from | Reaches |
|---|---|---|
| 1 | variant: 'bordered' | The variant the grid was asked for |
| 2 | defineDataGridConfig({ ui }) | Every grid in the app |
| 3 | <DataGrid {ui} /> | This grid only |
Density
Density is state on the grid rather than a class, and it writes two CSS variables that the row and cell slots read. Switch it below and watch the rows change height without a re-render of their content.
<!-- Density drives row height and cell padding through two CSS variables:
--dg-row-h and --dg-cell-py.
compact 2rem 0.25rem
standard 2.5rem 0.5rem
comfortable 3rem 0.75rem -->
<DataGrid data={people} {columns} {getRowId} density="compact" />
<!-- On an instance, density is state you can set -->
<script lang="ts">
const grid = createDataGrid<Person>({ data, columns, getRowId, density: 'comfortable' });
grid.density = 'compact';
</script>
<!-- The toolbar's density toggle writes the same state -->
<Grid.DensityToggle />Header Height
A header is as tall as a row until you say otherwise. headerHeight sets the leaf header row and groupHeaderHeight sets each tier of header groups above it, both in pixels. The second counts per tier rather
than for the lot, so three tiers at 28 stand 84 tall. Leave either off and it follows the
row height, which is to say it follows density.
Reach for the first when a header holds more than a label, such as a title wrapped onto two lines. Reach for the second when the tiers hold less: group labels are short, and a stack of them at full row height stands taller than the rows it describes. Rows pinned to the top and the filter row sit under whatever the header adds up to.
// Both are pixels, and both follow the row height when left off.
const grid = createDataGrid<Person>({
data,
columns,
getRowId,
headerHeight: 56, // the leaf header row
groupHeaderHeight: 28 // each tier of header groups, not the lot
});
// State on the grid, like density, so either can change later.
grid.headerHeight = 40;
grid.groupHeaderHeight = undefined; // back to following the rows
// The shorthand form takes the same two props:
// <DataGrid data={people} {columns} {getRowId} headerHeight={56} />
// What they write, on the grid's root element:
// --dg-header-h read by the headerCell slot
// --dg-group-header-h read by the groupCell slotNeither is written into a snapshot. Density is a preference a reader sets and expects to find again; a header height is a decision the page made about its own columns.
Data-Driven Classes
rowClass styles a whole row from its data, and cellClass one column's cells. Suspended rows are dimmed below, and salaries over 120,000 are picked out
in the success colour.
// Per row, passed to createDataGrid or to DataGrid in the shorthand form
createDataGrid<Person>({
columns,
data,
getRowId,
rowClass: (node) => (node.row.status === 'suspended' ? 'opacity-60' : '')
});
// Per cell, on the column that owns it
{
id: 'salary',
type: 'currency',
cellClass: ({ value }) => (Number(value) > 120000 ? 'font-semibold text-success' : '')
}
// Both run per render, so keep them cheap: no allocation, no lookups.The DOM Contract
Three attributes are public and will not change under you: data-dg-cell holds the absolute row and column index within the filtered and sorted set, and data-dg-row-id holds the row id. The third names a header group and is described below. Delegate from a
wrapper rather than attaching a handler per cell.
The two rows above the body use negative indices on the same attribute: -1 for the leaf header row and -2 for the filter row, so a row index below zero is never a data row. A header group spans
columns and cannot be named by a column index at all, so its cells carry data-dg-header-cell="level:firstColumn" instead, counted from the topmost level.
One handler on the wrapper below reads both attributes off whatever cell you click. Nothing
else in the markup is public: class names, element nesting and the transform the body uses
are all free to change between releases. That includes every other data-dg-* marker you may find in the markup. They are internal structure and carry no promise.
nothing clicked yet // Body cells carry their absolute position within the filtered and sorted
// set, and rows carry their id. Both are public, and so is the header
// group attribute further down. Every other data-dg-* marker is internal.
//
// <div data-dg-cell="rowIndex:colIndex">
// <div data-dg-row-id="42">
// The two rows above the body use negative indices on the same attribute:
//
// -1 the leaf header row
// -2 the filter row, when filtering({ floatingRow: true }) draws one
//
// so a row index below zero is never a data row.
// A header group spans columns, so a column index cannot name one. Its
// cells carry their own attribute instead, counted from the topmost level:
//
// <div data-dg-header-cell="level:firstColumn">
// Delegate from a wrapper rather than attaching a handler per cell:
function onPointerDown(event: PointerEvent) {
const cell = (event.target as HTMLElement).closest('[data-dg-cell]');
if (!cell) return;
const [rowIndex, colIndex] = cell.getAttribute('data-dg-cell').split(':').map(Number);
const rowId = cell.closest('[data-dg-row-id]')?.getAttribute('data-dg-row-id');
}Slots
Every key ui and defineDataGridConfig accept. A slot not listed here is internal. The last six style body cells rather than chrome: a feature paints them through cellDecoration as the grid renders, so they are only ever seen with that feature registered.
| Slot | What it styles |
|---|---|
root | The outermost element, which is where a virtual grid takes its height |
toolbar | The toolbar row above the viewport |
viewport | The scroller carrying role="grid" and the outer border |
header / headerRow / headerCell | The sticky header group, its row and its cells |
groupRow / groupCell / groupContent | A nested header group row, its cells, and the box a headerGroupCell snippet draws into |
groupCellFoldable / groupToggle | Room kept at the trailing edge of a group that can fold, and the control that sits in it |
rail / railHead / railEdge / railSurface | A group folded to a drawer: the strip down the body, its head over the header rows, the edges it closes itself with, and every cell standing over it |
railInner / railLabel / railFocus | What the drawer holds, the group's name turned to read up it, and how it marks focus without drawing a second box around a seam |
sortButton / menuButton / resizeHandle | The controls inside a header cell |
body / bodyOffset / row / cell | The body group, its transform, a row and a cell |
cellFocus / cellEditing / cellEditor / cellError | A focused cell, one being edited, its editor and its message |
rowEditing / cellEditorInRow / cellEditorInRowWidget / cellEditorInRowDivider | A row being edited: the ring around it, a text field inside it, a widget field inside it, and the hairline between two fields. A Select or a date field draws its own border and focus state, so the widget slot deliberately adds neither |
cellEditorFlat / cellEditorPad / cellEditorWide / cellEditorField / cellEditable | A text editor filling its cell, a widget editor padded inside one, an editor wider than its column, the field itself, and the hint on a cell that can be edited |
pinnedCell / pinnedHeaderCell / pinnedRow / pinnedRowsTop / pinnedRowsBottom | The pinned sections: a pinned cell, its header, a pinned row and the two bands |
stickyGroupRows / stickyGroupRow | The band stickyGroups pins under the header, and one line in it for each group the reader is inside |
flashCell | A cell flashCells() is marking. It carries the class the fade is attached to, so classes added here join every mark |
pinnedCellRaised / pinnedCellSelected | A pinned cell over scrolled content, and one in a selected row |
rowSelected / rowDragging / rowGhost | Selection tint, the row being dragged and its lifted copy |
rowSpanFill / rowSpanFillLast / rowSpanEdge / rowSpanEdgeStart / cellRowSpan | A merged block, its last row, the edges it draws, the edge it opens with, and the cell that spans |
headerControls / headerControlsPinned / headerDivider / groupBoundary | The control cluster in a header cell, the same over a pinned column, the line between two headers, and the heavier line where a group ends |
rowHandle / dropIndicator / rowDropIndicator | The drag grip, and the two lines that say where a column or a row would land |
chooserItem / toggleButton / tooltipTrigger / fullWidthCell | A row in the column chooser, the expand toggle on a nested row, the wrapper an explicit tooltip attaches to, and the single cell a full-width row draws |
filterRow / filterCell / filterCellPinned / filterSummary | The filter row under the header, a cell of it, the same over a pinned column, and the text a column whose filter the row cannot hold reads back as |
filterPanel / filterChips / statusBar / footer | The filter surfaces and the chrome under the grid |
empty | The no-rows surface |
groupPanel / groupPanelLabel / groupPanelChip / groupPanelChipLabel | The drop area above the grid that grouping() groups from, its caption, and a chip naming one grouped column. Grid.PivotPanel draws its four zones with the same slots |
groupPanelSeparator / groupPanelEmpty | The chevron between two chips, which reads as the nesting order, and the prompt shown while nothing is grouped |
toolPanel / toolPanelCollapsed / toolPanelHeader / toolPanelTitle / toolPanelTabs | The side panel, the narrow bar it folds to, and its header, caption and tab strip. It is a column beside the grid from lg up and a full-width block below that |
toolPanelBody / toolPanelRow / toolPanelRowLabel / toolPanelRowMuted | The scrolling list, one row of it, that row's label, and the dimming a row takes when what it offers does not apply |
toolPanelValueRow / toolPanelPinMark / toolPanelHint | A row on the values tab, which stacks a column name over its aggregation picker, the left or right mark on a pinned column, and the note under the list |
filterBuilder / filterBuilderGroup / filterBuilderGroupHeader / filterBuilderRow | The condition tree advancedFilter() draws: the panel, a nested group, the row carrying that group's join and its controls, and one condition |
filterBuilderColumnSelect / filterBuilderOperatorSelect / filterBuilderValueInput | The three controls of a condition. The two selects hold a fixed width so a tree of conditions lines up; the value field takes what is left |
filterBuilderValuePlaceholder / filterBuilderHint | The stand-in shown where an operator such as blank needs no value, and the note under a group |
findPanel / findCount | The find and replace bar, and its tally. The count is tabular so it does not shift as the numbers change while typing |
formatPanel / formatPanelLabel / formatPanelEmpty / formatPanelError | The conditional formatting bar, its caption, the prompt shown with no rules, and the message an expression rule that will not parse reads back as |
formatRuleChip / formatRuleChipLabel / formatRuleChipInvalid | A chip naming one rule, its label, and the ring the chip takes when that rule cannot be applied |
importPanel / importDropZone / importDropZoneActive / importHint | The import wizard, the area a file is dropped on, that area while a file is over it, and the note under it |
importMapRow / importMapLabel / importMapSelect | One row of the mapping step: the column being mapped, its name, and the picker choosing which field of the file feeds it |
importIssueList / importIssueRow / importIssueCount | The review step's list of what failed validation, one entry of it, and the tally beside it, which is tabular and coloured as an error |
savedViews / savedViewsLabel / savedViewsModified | The saved views bar, its caption, and the mark saying the grid has moved since the open view was saved |
rangeStatusBar / rangeStatusBarValue | The strip under the grid summing the selected range, and one figure in it |
commandPalette | The command palette dialog |
rangeCell / rangeEdgeTop / rangeEdgeBottom / rangeEdgeStart / rangeEdgeEnd | A cell inside a selected range, and the four edges. Each edge is drawn only where the range actually ends, so several ranges each keep their own outline |
fillHandle / fillPreview | The square at the trailing corner of a range that starts a fill, and the cells the fill would write if released now |
cutSource / moveTarget | The range a cut lifted from, and the cells it would land on |
findMatch / findMatchCurrent | A cell find matched, and the one the cursor is on |
formatHighlight | A cell an expression or duplicates rule matched. A colour scale and a data bar paint through cellDecoration.style instead, because the value they need cannot be named by a class |
importStagedCell / importBadCell | A cell holding a row that has been staged by the import wizard but not committed, and one whose value failed its column's schema |
CSS Variables
| Variable | Meaning |
|---|---|
--dg-row-h | Row height. 2rem compact, 2.5rem standard, 3rem comfortable |
--dg-header-h | Height of the leaf header row. Follows --dg-row-h until headerHeight sets it in pixels |
--dg-group-header-h | Height of one header group tier. Follows --dg-row-h until groupHeaderHeight sets it in pixels |
--dg-flash-tint / --dg-flash-ms | On a cell flashCells() is marking: the colour of that mark and how long it lasts. The fade reads both |
--dg-cell-py | Vertical cell padding. 0.25rem, 0.5rem, 0.75rem |
--dg-grid-template | The grid-template-columns every row uses, written by the column model |