Columns

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.

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.

Name
Initials
Annual salary
Hoang Kowalski
HK
$127,691.00
Bruno Nguyen
BN
$105,146.00
Bruno Dubois
BD
$86,538.00
Farid Haddad
FH
$76,443.00
Jonas Yilmaz
JY
$129,812.00
5 rows
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.

ID
Name (flex 1)
Email (flex 2)
Team (fixed 140)
1
Hoang Kowalski
hoang.kowalski1@example.com
Design
2
Bruno Nguyen
bruno.nguyen2@example.com
Growth
3
Bruno Dubois
bruno.dubois3@example.com
Design
4
Farid Haddad
farid.haddad4@example.com
Growth
5
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
5 rows
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.

Left
Center
Right
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
5 rows
// 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-aligned

Pinning

Scroll the grid sideways: Member stays on the left edge, Salary on the right, and the middle section moves under them.

Member
Email
Team
Role
Country
Joined
Salary
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
Poland
Mar 6, 2024
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
Brazil
Mar 6, 2024
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
Nigeria
Feb 5, 2021
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
Brazil
May 4, 2022
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
Vietnam
Nov 13, 2024
$129,812.00
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
France
Feb 17, 2019
$72,011.00
6 rows
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.

Name
Team
Status
Hoang Kowalski
Design
invited
Bruno Nguyen
Growth
invited
Bruno Dubois
Design
active
Farid Haddad
Growth
suspended
Jonas Yilmaz
Core
active
5 rows
<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.

FieldType
nodeRowNode<TRow>
rowTRow
valueunknown
rowIndexnumber
columnColumnState<TRow>
formattedstring | 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.

Name
Team
Salary
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
Mai Nguyen
Support
$83,226.00
Bruno Novak
Support
$71,507.00
8 rows
// 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.

Team
Name
Salary
Core
Jonas Yilmaz
$129,812.00
Bruno Yilmaz
$139,212.00
Niko Muller
$76,769.00
Keiko Novak
$125,071.00
Lucas Muller
$59,734.00
Hoang Rossi
$130,572.00
Design
Hoang Kowalski
$127,691.00
Bruno Dubois
$86,538.00
Tuan Ivanov
$105,520.00
Quyen Silva
$121,817.00
Chi Silva
$130,525.00
Growth
Bruno Nguyen
$105,146.00
12 rows
// 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.

Email (auto)
Name (always)
Salary (formatted)
Team (custom)
hoang.kowalski1@example.com
Hoang Kowalski
$127,691.00
Design
bruno.nguyen2@example.com
Bruno Nguyen
$105,146.00
Growth
bruno.dubois3@example.com
Bruno Dubois
$86,538.00
Design
farid.haddad4@example.com
Farid Haddad
$76,443.00
Growth
jonas.yilmaz5@example.com
Jonas Yilmaz
$129,812.00
Core
5 rows
// 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.

PropertyDefault
id-
headerthe column id
headerCell-
accessorrow[id]
width-
flex1 when width is omitted
minWidth40
maxWidth-
align'left'
hiddenfalse
pinned-
children-
columnGroupShow-
collapsedfalse
collapseMode'summary'
headerGroupCell-
resizabletrue
tooltipon overflow
meta-
sortablefalse
sortFn-
sortField-
filter-
type-
typeOptions-
cell-
cellClass-
colSpan-
rowSpan-
editablefalse
editor'text'
schema-
validate-
parse-