Customization

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.

Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
Quyen Tanaka
Growth
$72,011.00
6 rows
<!-- 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:

PassWhere it comes fromReaches
1variant: 'bordered'The variant the grid was asked for
2defineDataGridConfig({ 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.

Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
Quyen Tanaka
Growth
$72,011.00
6 rows
<!-- 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.

Name
Status
Salary
Hoang Kowalski
invited
$127,691.00
Bruno Nguyen
invited
$105,146.00
Bruno Dubois
active
$86,538.00
Farid Haddad
suspended
$76,443.00
Jonas Yilmaz
active
$129,812.00
Quyen Tanaka
suspended
$72,011.00
6 rows
// 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
Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
Quyen Tanaka
Growth
$72,011.00
6 rows
// 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.

SlotWhat it styles
rootThe outermost element, which is where a virtual grid takes its height
toolbarThe toolbar row above the viewport
viewportThe scroller carrying role="grid" and the outer border
header / headerRow / headerCellThe sticky header group, its row and its cells
groupRow / groupCell / groupContentA nested header group row, its cells, and the box a headerGroupCell snippet draws into
groupCellFoldable / groupToggleRoom kept at the trailing edge of a group that can fold, and the control that sits in it
rail / railHead / railEdge / railSurfaceA 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 / railFocusWhat 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 / resizeHandleThe controls inside a header cell
body / bodyOffset / row / cellThe body group, its transform, a row and a cell
cellFocus / cellEditing / cellEditor / cellErrorA focused cell, one being edited, its editor and its message
rowEditing / cellEditorInRow / cellEditorInRowWidget / cellEditorInRowDividerA 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 / cellEditableA 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 / pinnedRowsBottomThe pinned sections: a pinned cell, its header, a pinned row and the two bands
pinnedCellRaised / pinnedCellSelectedA pinned cell over scrolled content, and one in a selected row
rowSelected / rowDragging / rowGhostSelection tint, the row being dragged and its lifted copy
rowSpanFill / rowSpanFillLast / rowSpanEdge / rowSpanEdgeStart / cellRowSpanA merged block, its last row, the edges it draws, the edge it opens with, and the cell that spans
headerControls / headerControlsPinned / headerDivider / groupBoundaryThe 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 / rowDropIndicatorThe drag grip, and the two lines that say where a column or a row would land
chooserItem / toggleButton / tooltipTrigger / fullWidthCellA 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 / filterSummaryThe 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 / footerThe filter surfaces and the chrome under the grid
emptyThe no-rows surface

CSS Variables

VariableMeaning
--dg-row-hRow height. 2rem compact, 2.5rem standard, 3rem comfortable
--dg-cell-pyVertical cell padding. 0.25rem, 0.5rem, 0.75rem
--dg-grid-templateThe grid-template-columns every row uses, written by the column model