Column Definition
A column is a plain object. The same definition carries its sizing, its renderer, its filter, its sort and its editor, so a column is one thing to read rather than five parallel maps.
collapsed, collapseMode and headerGroupCell, and any child of one can carry columnGroupShow. All four are on Header Groups, which is a page of its own now.Anatomy
id is
the only required field, and doubles as the row property key. Give the column an accessor when the value is computed rather than stored.
import type { ColumnDef } from '@sv5ui/datagrid';
const columns: ColumnDef<Person>[] = [
// id doubles as the row property key
{ id: 'name', header: 'Name' },
// accessor takes over when the value is not a plain property
{
id: 'fullName',
header: 'Full name',
accessor: (person) => `${person.first} ${person.last}`
},
// header is the accessible name, so it stays plain text even when
// headerCell draws something else
{ id: 'salary', header: 'Annual salary', align: 'right', type: 'currency' }
];Sizing
Fixed columns take their width first; flex columns share what is left in proportion to their
weight. Drag any header edge to resize, or double-click it to autosize. The Team column
below sets resizable: false, so it does not move.
const columns: ColumnDef<Person>[] = [
// Fixed: never grows, never shrinks below minWidth
{ id: 'id', header: 'ID', width: 80 },
// Flex: shares whatever the fixed columns leave over.
// flex 2 takes twice the leftover space of flex 1.
{ id: 'name', header: 'Name', flex: 1, minWidth: 140 },
{ id: 'email', header: 'Email', flex: 2, minWidth: 200 },
// maxWidth applies to fixed widths, including after a resize
{ id: 'team', header: 'Team', width: 140, maxWidth: 200 },
// resizable: false freezes one column while the rest stay draggable
{ id: 'status', header: 'Status', width: 110, resizable: false }
];
// flex defaults to 1 when width is omitted, so a column with neither
// still shares the leftover space.Alignment
One property moves the header label and the cell content together, so a right-aligned number column keeps its label over the digits.
// align moves the header label and the cell content together
{ id: 'name', header: 'Name', align: 'left' } // default
{ id: 'active', header: 'Active', align: 'center' }
{ id: 'salary', header: 'Salary', align: 'right' } // numbers read better right-alignedPinning
Scroll the grid sideways: Member stays on the left edge, Salary on the right, and the middle section moves under them.
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Member', width: 200, pinned: 'left' },
{ id: 'email', header: 'Email', width: 260 },
{ id: 'team', header: 'Team', width: 140 },
{ id: 'role', header: 'Role', width: 140 },
{ id: 'country', header: 'Country', width: 160 },
{ id: 'joinedAt', header: 'Joined', width: 140, type: 'date' },
{ id: 'salary', header: 'Salary', width: 140, align: 'right',
type: 'currency', pinned: 'right' }
];
// Pinned columns stay put while the middle section scrolls. Under
// dir="rtl" the sides mirror, because the layout uses logical properties.Header Groups
A column with children becomes a group header spanning its leaves, and a group can fold: down to a summary column, or
away entirely behind a drawer with its name down the side. That is a page of its own.
Custom Header and Cell
headerCell draws the label while header stays the accessible name. cell always wins over type, so
a column can graduate to a custom renderer without changing anything else.
<script lang="ts">
import { DataGrid, type ColumnDef, type DataGridCellContext } from '@sv5ui/datagrid';
</script>
<!-- headerCell draws the label; header stays the accessible name -->
{#snippet teamHeader()}
<span class="flex items-center gap-1.5">
<Icon name="lucide:users" class="size-3.5" />
Team
</span>
{/snippet}
<!-- cell receives node, row, value and rowIndex, and wins over type -->
{#snippet statusCell({ row }: DataGridCellContext<Person>)}
<Badge
label={row.status}
size="sm"
variant="soft"
color={row.status === 'active' ? 'success' : row.status === 'invited' ? 'info' : 'error'}
/>
{/snippet}
<DataGrid
data={people}
{getRowId}
columns={[
{ id: 'name', header: 'Name', flex: 1 },
{ id: 'team', header: 'Team', width: 140, headerCell: teamHeader },
{ id: 'status', header: 'Status', width: 140, cell: statusCell }
]}
/>Cell Context
What a cell snippet, cellClass, tooltip, colSpan and rowSpan all receive.
| Field | Type |
|---|---|
node | RowNode<TRow> |
row | TRow |
value | unknown |
rowIndex | number |
column | ColumnState<TRow> |
formatted | string | undefined |
Data-Driven Classes
cellClass styles one column's cells from their own value; rowClass styles
a whole row. Both run per render, so keep them cheap.
// Per cell: runs for every rendered cell of this column, so keep it cheap
{
id: 'salary',
header: 'Salary',
type: 'currency',
align: 'right',
cellClass: ({ value }) => (Number(value) > 120000 ? 'font-semibold text-success' : '')
}
// 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' : '')
});Spanning
rowSpan(ctx) and colSpan(ctx) return how many cells to merge from the current one. Covered cells are not rendered, the merged
cell carries the matching ARIA attribute, and it is the single tab stop for the block.
// How many rows from this index share the same team. A run only starts
// where the value changes; inside one, the cell is covered and its span
// is never asked for.
function runLength(index: number): number {
if (index > 0 && rows[index - 1].team === rows[index].team) return 1;
let n = 1;
while (index + n < rows.length && rows[index + n].team === rows[index].team) n++;
return n;
}
const columns: ColumnDef<Person>[] = [
{ id: 'team', header: 'Team', width: 140, rowSpan: (ctx) => runLength(ctx.rowIndex) },
{ id: 'name', header: 'Name', flex: 1 },
{ id: 'salary', header: 'Salary', width: 130, align: 'right', type: 'currency' }
];
// colSpan(ctx) works the same way across columns, and is clamped so a
// merged cell never crosses a pin boundary.Tooltips
Left unset, a cell whose text is cut off is titled on hover: narrow the first column below
and the email gets one. That one stays a native title,
because it can fire on any cell in the grid and is measured on hover.
An explicit tooltip is
a real component instead, so the design system can style it, and it shows what the cell shows:
the Salary column reads $127,691.00 rather than the number behind it. A function receives formatted alongside the raw value. It costs a component per cell, so it is opt-in per column.
// Unset: a cell whose text is cut off is titled on hover
{ id: 'email', header: 'Email', flex: 1 }
// true: always titled, cut off or not
{ id: 'name', header: 'Name', tooltip: true }
// A function decides the text
{ id: 'team', header: 'Team', tooltip: ({ row }) => `${row.team} team, ${row.country}` }
// false turns it off
{ id: 'avatar', header: '', tooltip: false }Hidden Columns and Metadata
A hidden column stays in the model, so the column chooser can bring it back and a CSV export
can still name it. meta carries whatever your app needs to attach; the grid never reads it.
Open the column chooser in any toolbar demo to toggle visibility at runtime.
// hidden keeps the column in the model but out of the render, so the
// column chooser can bring it back and CSV export can still name it.
{ id: 'country', header: 'Country', width: 150, hidden: true }
// meta travels with the definition. The grid never reads it.
{ id: 'salary', header: 'Salary', meta: { source: 'payroll', sensitive: true } }
// Toggle from code
grid.api.setColumnHidden?.('country', false);ColumnDef Reference
Sorting, filtering and editing fields are covered in depth on their own pages.
| Property | Default |
|---|---|
id | - |
header | the column id |
headerCell | - |
accessor | row[id] |
width | - |
flex | 1 when width is omitted |
minWidth | 40 |
maxWidth | - |
align | 'left' |
hidden | false |
pinned | - |
children | - |
columnGroupShow | - |
collapsed | false |
collapseMode | 'summary' |
headerGroupCell | - |
resizable | true |
tooltip | on overflow |
meta | - |
sortable | false |
sortFn | - |
sortField | - |
filter | - |
type | - |
typeOptions | - |
cell | - |
cellClass | - |
colSpan | - |
rowSpan | - |
editable | false |
editor | 'text' |
schema | - |
validate | - |
parse | - |