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.
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 />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
Two 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. 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.
nothing clicked yet // Body cells carry their absolute position within the filtered and sorted
// set, and rows carry their id. Both are public.
//
// <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.
| 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 |
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 |
CSS Variables
| Variable | Meaning |
|---|---|
--dg-row-h | Row height. 2rem compact, 2.5rem standard, 3rem comfortable |
--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 |