Schema mode
The TableSchema reference, the declarative config behind <DataTable>.
<DataTable schema={…} data={…} /> renders a complete table from a single JSON-serializable object. The schema is strict: unknown keys are rejected, so a mistyped or invented prop produces an error instead of quietly doing nothing.
import { parseTableSchema } from "@tablekit/core/schema";
const result = parseTableSchema(schema);
if (!result.success) console.error(result.errors);
// → ["columns.2.type: Invalid option: expected one of \"text\"|\"number\"|…"]
The JSON Schema version is available at /schema/v1.json and as @tablekit/core/tablekit.schema.json. Use it for editor autocompletion ("$schema": "…/schema/v1.json") or for LLM structured output.
Top level
| Key | Type | Default | Notes |
|---|---|---|---|
title |
string | Toolbar heading and the table’s accessible name. | |
description |
string | Muted text under the title. | |
rowId |
string | "id" |
Row field used as a stable id (for selection). |
columns |
Column[] | required | At least one. |
features |
object | see below | |
appearance |
object | see below | |
initialState |
{ sort?, search? } |
sort: [{ id, desc }]. |
|
bulkActions |
{ id, label, tone? }[] |
Shown in the selection bar. Requires selection: "multi". |
|
emptyState |
{ title, description? } |
Shown when there’s no data. This is different from “no results”. |
features
| Key | Default | |
|---|---|---|
search |
true |
Global search box |
columnFilters |
true |
Filters popover and chips |
sorting / multiSort |
true / true |
Shift+click adds a sort |
pagination |
true |
|
pageSize / pageSizeOptions |
10 / [10,25,50,100] |
|
selection |
"none" |
"single" or "multi" |
columnVisibility |
true |
Show/hide checkboxes in the Columns menu |
columnReorder |
true |
Drag handles in the Columns menu (or ↑/↓ on the handle) |
columnPinning |
true |
Pin toggles in the Columns menu; column.pinned sets the initial state |
columnResize |
true |
Drag, or use arrow keys on the handle |
stickyHeader |
true |
Sticks inside maxHeight |
appearance
| Key | Default | |
|---|---|---|
density |
"default" |
"compact" or "comfortable" |
variant |
"plain" |
"zebra" or "bordered" |
responsive |
"stack" |
"scroll" or "priority". See Responsive |
stackBelow |
640 |
Container width in px |
Columns
Every column has field, header and type. field accepts dot paths such as customer.name.
type |
Renders | Default filter | Type-specific keys |
|---|---|---|---|
text |
Plain text | none | format.prefix/suffix |
number |
Intl.NumberFormat, right-aligned |
range | format.decimals/notation/style/unit |
currency |
Money, right-aligned | range | format.currency (ISO 4217) |
date |
<time> with a full timestamp on hover |
range (date inputs) | format.dateStyle (relative!) / timeStyle |
badge |
Tone-colored pill with a dot, an icon, or icon only | select | badge.tones, badge.labels, badge.defaultTone, badge.indicator (dot · icon · icon-only · none), badge.icons (value → icon name), badge.fill (default true), badge.stroke (default false) |
avatar |
Initials or image, plus a subtitle. logo: "inline" (16px mark on the name’s first line; --tk-avatar-logo-inline-size to change) or logo: "circle" (mark in a filled circle) for bank/merchant logos |
none | avatar.imageField, avatar.imageDarkField (dark-theme variant, e.g. a light logo), avatar.subtitleField, avatar.logo, avatar.logoFill (neutral · accent) |
link |
Anchor | none | link.hrefTemplate (/x/{id}), link.hrefField, link.external |
boolean |
Check or dash icon | select | format.trueLabel/falseLabel |
actions |
Row menu (⋯) or inline icon buttons, never a row of text buttons | none | actions: [{ id, label, tone?, icon?, disabled?, separator?, when? }], actionsDisplay (menu · inline) |
button |
One button inside the cell (Pay, Download, Retry) | none | button: { id, label?, variant? (secondary · primary · ghost), tone?, icon?, when? } |
Shared column keys: id, sortable, filter ("text" | "select" | "range" | false), searchable, hideable, hidden, width, minWidth, maxWidth, pinned, priority (1–5), align.
Icons come from Lucide, from two closed lists so a generated schema can’t name an icon that doesn’t exist:
- badge icons:
circle-check,check,circle-x,x,triangle-alert,circle-alert,info,circle-dashed,circle,circle-dot,circle-pause,clock,hourglass,loader(spins),refresh-cw,ban,lock,shield-check,arrow-up,arrow-down,arrow-right,undo-2,send,truck,star,zap,sparkles,eye. Withindicator: "icon"and no per-value icon, each tone uses its default: neutralcircle-dashed, infoinfo, successcircle-check, warningtriangle-alert, dangercircle-x, accentsparkles. - action icons:
eye,pencil,copy,download,upload,share-2,send,external-link,refresh-cw,undo-2,archive,trash-2,ban,lock,unlock,check,x,user-plus,mail,receipt,flag,star. Every action needs one whenactionsDisplayisinline.
An action’s when: { field, in } shows it only on matching rows, e.g. Refund only where status is paid.
Tones are semantic: neutral, info, success, warning, danger, accent. They map to --tk-tone-* tokens, so each design system decides the actual colors. Never put hex colors in a schema.
Events
<DataTable
schema={schema}
data={rows}
onAction={(actionId, row) => {}} // row menu
onBulkAction={(actionId, rows) => {}} // selection bar
onRowClick={(row) => {}}
onSelectionChange={(ids) => {}}
loading={isLoading}
error={error}
onRetry={refetch}
/>