Skip to content

Data columns

A data column reads a value from each row and renders it. It’s the default kind — set an accessorKey (or accessorFn), give it a header, and you’ve got a sortable, filterable, editable column.

When the value is a top-level property of your row type, use accessorKey. TypeScript narrows the allowed strings to the keys of TData:

type Person = { id: number; firstName: string; lastName: string };
const columns: SST_ColumnDef<Person>[] = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'firstName', header: 'First' },
{ accessorKey: 'lastName', header: 'Last' },
];

accessorKey doubles as the column id unless you override it explicitly.

accessorFn — for computed / nested / joined values

Section titled “accessorFn — for computed / nested / joined values”

For anything that isn’t a direct property read — a derived value, a deep path, a joined string — use accessorFn. Pair it with an explicit id:

const columns: SST_ColumnDef<Person>[] = [
{
id: 'fullName',
accessorFn: (row) => `${row.firstName} ${row.lastName}`,
header: 'Full name',
},
{
id: 'isAdult',
accessorFn: (row) => row.age >= 18,
header: 'Adult?',
},
];

The accessed value is what sorting, filtering, and grouping operate on. The Cell renderer (if provided) gets the same value via getValue().

Custom rendering — separating data from presentation

Section titled “Custom rendering — separating data from presentation”

accessorKey / accessorFn decide what the value is. Cell decides how it looks:

{
accessorKey: 'status',
header: 'Status',
Cell: ({ cell }) => {
const value = cell.getValue<'active' | 'paused'>();
return <Badge variant={value === 'active' ? 'default' : 'secondary'}>{value}</Badge>;
},
}

Keep Cell for presentation. Sorting + filtering still use the raw accessed value (faster, more predictable). If you need filtering on the rendered string instead of the raw value, use the column’s filterFn.

A column with a columns array becomes a header group:

{
header: 'Name',
columns: [
{ accessorKey: 'firstName', header: 'First' },
{ accessorKey: 'lastName', header: 'Last' },
],
}

Grouped headers render as a banner row above their child columns. The group itself isn’t sortable or filterable — the children are.

The columns array and the data array passed to useShadStackTable must be referentially stable across renders. Re-creating them on every render re-runs the entire table engine:

const columns = useMemo(() => [...], []); // ✅
const data = useMemo(() => fetched, [fetched]); // ✅
const columns = [...]; // ❌ new array every render