Table
A native table in server-rendered parts, for orders, products and customers. Rows can link to a detail page as a whole, columns sort through the URL, and number columns line up. No client JS.
Import
import { Table } from '@nizivo/react/table';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/table.css';Static or interactive
Server. Every part renders native table markup with no client JavaScript: Table.Container (a
<div>, the border and the horizontal scroll), Table.Root (<table>), Table.Caption,
Table.Header, Table.Body, Table.Footer, Table.Row, Table.Head (<th>), Table.Cell,
Table.SortButton and Table.RowLink. It is a table, not a grid: arrow keys do not move between
cells, and assistive technology reads it as a table. Row checkboxes for bulk actions are a subpath of
their own, TableSelection, so a page that imports
@nizivo/react/table loads no client module in any bundler.
How to choose.
In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.
Name every table
Give the table a Table.Caption. When a card heading already says the same words, keep the caption
and pass visuallyHidden, or drop it and point the table at the heading with aria-labelledby.
Rows that link
<Table.Row interactive> with a Table.RowLink in it (usually the first cell, as Figma's Order Row
draws the order number) makes the whole row the link's hit area and shows bg/hover on hover.
Buttons and other links in the row stay above it. Pass the router's link with
render={<Link href="/orders/1042" />}.
Sorting
In the URL (a Server Component, or any page whose sort should survive a reload or a shared link):
put a Table.SortButton in each sortable Table.Head, with href to the same page sorted by that
column (render={<Link href="…" />} for the router's link), and pass sort="ascending" or
sort="descending" to the column the page is sorted by: its <th> gets aria-sort, and the arrow
follows.
In state (a client component): with neither href nor render, Table.SortButton is a
<button type="button"> for your own handler. Keep the column and direction in useState, sort
the rows, and pass sort to the sorted column's Table.Head:
<Table.Head sort={column === 'name' ? direction : undefined}>
<Table.SortButton onClick={() => sortBy('name')}>Product</Table.SortButton>
</Table.Head>In a client app has the whole example, live.
Numbers and dates
numeric on a Table.Head and the Table.Cells under it end-aligns them in tabular figures
(Numeric/M in cells). Format dates and money with Intl and an explicit timeZone, so the server
and the browser print the same text.
Layout
Figma: a 12 px radius with a border/subtle stroke; the header on bg/subtle in Label/M; cells
12 / 20 px, a border/subtle line under each row. A table wider than its container scrolls inside
it; when its rows hold no link or control, give Table.Container a label, which names it as a
scrollable region and puts it in the Tab order, so a keyboard user can reach it and scroll it with the
arrow keys (Safari and Firefox do not focus a scroller on their own). selected on a row paints bg/brand-subtle. Hooks: --nz-table-bg, --nz-table-border,
--nz-table-radius, --nz-table-header-bg, --nz-table-cell-padding-block,
--nz-table-cell-padding-inline, --nz-table-row-bg-hover, --nz-table-row-bg-selected.
Examples
Orders
08's Recent orders (Figma Order Row): each row links to its order through the order number, a stretched link, so the whole row is the hit area. The caption names the table and is hidden, because the card above it shows the same words.
import type { JSX } from 'react';
import { Avatar } from '@nizivo/react/avatar';
import { Badge, type BadgeTone } from '@nizivo/react/badge';
import { Inline } from '@nizivo/react/inline';
import { Table } from '@nizivo/react/table';
interface Order {
id: string;
placed: string;
customer: string;
payment: [string, BadgeTone];
fulfilment: [string, BadgeTone];
total: number;
}
/** Fixed data, as a server would load it; times are UTC instants. */
const ORDERS: readonly Order[] = [
{
id: '1042',
placed: '2026-10-14T09:12:00Z',
customer: 'Maya Rao',
payment: ['Paid', 'success'],
fulfilment: ['Unfulfilled', 'warning'],
total: 112,
},
{
id: '1041',
placed: '2026-10-14T08:47:00Z',
customer: 'Jonah Okafor',
payment: ['Paid', 'success'],
fulfilment: ['Fulfilled', 'success'],
total: 64.5,
},
{
id: '1040',
placed: '2026-10-13T17:05:00Z',
customer: 'Priya Shah',
payment: ['Pending', 'warning'],
fulfilment: ['On hold', 'neutral'],
total: 238,
},
{
id: '1039',
placed: '2026-10-13T11:30:00Z',
customer: 'Leo Martin',
payment: ['Refunded', 'neutral'],
fulfilment: ['Fulfilled', 'success'],
total: 28,
},
];
// An explicit locale and time zone: the server and the browser print the same text (§16.3).
const WHEN = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZone: 'UTC',
});
const MONEY = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
export function TableOrdersExample(): JSX.Element {
return (
<Table.Container>
<Table.Root>
<Table.Caption visuallyHidden>Recent orders</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Order</Table.Head>
<Table.Head>Date</Table.Head>
<Table.Head>Customer</Table.Head>
<Table.Head>Payment</Table.Head>
<Table.Head>Fulfilment</Table.Head>
<Table.Head numeric>Total</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{ORDERS.map((order) => (
<Table.Row key={order.id} interactive>
<Table.Cell>
<Table.RowLink href={`#order-${order.id}`}>#{order.id}</Table.RowLink>
</Table.Cell>
<Table.Cell>
<time dateTime={order.placed}>{WHEN.format(new Date(order.placed))}</time>
</Table.Cell>
<Table.Cell>
<Inline gap="2">
<Avatar name={order.customer} size="sm" decorative />
{order.customer}
</Inline>
</Table.Cell>
<Table.Cell>
<Badge tone={order.payment[1]}>{order.payment[0]}</Badge>
</Table.Cell>
<Table.Cell>
<Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
</Table.Cell>
<Table.Cell numeric>{MONEY.format(order.total)}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Table.Container>
);
}
Sort
Sorting through the URL: each column's label is a link to the same page sorted by it, and the sorted column's <th> carries aria-sort. The page reads ?sort= on the server; no client code.
import type { JSX } from 'react';
import { Table } from '@nizivo/react/table';
export function TableSortExample(): JSX.Element {
return (
<Table.Container>
<Table.Root>
<Table.Caption>Products, by price, highest first</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>
<Table.SortButton href="?sort=name">Product</Table.SortButton>
</Table.Head>
<Table.Head>
<Table.SortButton href="?sort=stock">Inventory</Table.SortButton>
</Table.Head>
<Table.Head numeric sort="descending">
<Table.SortButton href="?sort=price">Price</Table.SortButton>
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Head scope="row">Linen apron</Table.Head>
<Table.Cell>8 in stock</Table.Cell>
<Table.Cell numeric>$42.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Head scope="row">Stoneware mug</Table.Head>
<Table.Cell>124 in stock</Table.Cell>
<Table.Cell numeric>$28.00</Table.Cell>
</Table.Row>
<Table.Row selected>
<Table.Head scope="row">Ceramic coaster set</Table.Head>
<Table.Cell>Sold out</Table.Cell>
<Table.Cell numeric>$18.50</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</Table.Container>
);
}
Parts
Use the parts through the namespace (Table.Container) or by their flat names (TableContainer); both work from Server Components.
Table.Container
The table's box: the border, the radius and the horizontal scroll of a wide table.
Server component Renders data-slot="table-container" with the class nz-table-container. Props: TableContainerProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
label | string | – | Names the container as a scrollable region and puts it in the Tab order, so a keyboard can scroll a table wider than the screen (Safari and Firefox do not focus a scroller on their own). Give it when the table can scroll sideways and its rows hold no link or control. |
render | RenderProp<Empty> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Table.Root
Server component Renders data-slot="table" with the class nz-table. Props: TableRootProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – |
- Also takes every attribute of
<table>. (ComponentPropsWithRef<"table">)
render takes an element, or, in client code, a function of the props and no state.
Table.Caption
The table's name. Give every table one: a caption, or aria-labelledby to a visible heading.
Server component Renders data-slot="table-caption" with the class nz-table-caption. Props: TableCaptionProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – | |
visuallyHidden | boolean | false | Keeps the caption as the table's accessible name but hides it from sight, for a table whose card already shows a heading. |
- Also takes every attribute of
<caption>. (ComponentPropsWithRef<"caption">)
render takes an element, or, in client code, a function of the props and no state.
Table.Header
Server component Renders data-slot="table-header" with the class nz-table-header. Props: TableSectionProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – |
- Also takes every attribute of
<tbody>. (ComponentPropsWithRef<"tbody">)
render takes an element, or, in client code, a function of the props and no state.
Table.Body
Server component Renders data-slot="table-body" with the class nz-table-body. Props: TableSectionProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – |
- Also takes every attribute of
<tbody>. (ComponentPropsWithRef<"tbody">)
render takes an element, or, in client code, a function of the props and no state.
Table.Footer
Server component Renders data-slot="table-footer" with the class nz-table-footer. Props: TableSectionProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – |
- Also takes every attribute of
<tbody>. (ComponentPropsWithRef<"tbody">)
render takes an element, or, in client code, a function of the props and no state.
Table.Row
Server component Renders data-slot="table-row" with the class nz-table-row. Props: TableRowProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
interactive | boolean | false | The whole row is the hit area of its |
render | RenderProp<TableRowState> | – | |
selected | boolean | false |
|
- Also takes every attribute of
<tr>. (ComponentPropsWithRef<"tr">)
render takes an element, or, in client code, a function of the props and this state (TableRowState):
| Name | Type | Default | Description |
|---|---|---|---|
interactiverequired | boolean | – | |
selectedrequired | boolean | – |
Table.Head
Server component Renders data-slot="table-head" with the class nz-table-head. Props: TableHeadProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
numeric | boolean | false | A column of numbers: end-aligned, tabular figures. |
render | RenderProp<TableHeadState> | – | |
scope | "col" | "row" | "colgroup" | "rowgroup" | 'col' |
|
sort | TableSort"ascending""descending" | – | The column's current sort: |
- Also takes every attribute of
<th>;scopeis redeclared above. (ComponentPropsWithRef<"th">)
render takes an element, or, in client code, a function of the props and this state (TableHeadState):
| Name | Type | Default | Description |
|---|---|---|---|
numericrequired | boolean | – | |
sortrequired | TableSort | undefined"ascending""descending" | – |
Table.Cell
Server component Renders data-slot="table-cell" with the class nz-table-cell. Props: TableCellProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
numeric | boolean | false | A number: end-aligned, tabular figures. |
render | RenderProp<TableCellState> | – |
- Also takes every attribute of
<td>. (ComponentPropsWithRef<"td">)
render takes an element, or, in client code, a function of the props and this state (TableCellState):
| Name | Type | Default | Description |
|---|---|---|---|
numericrequired | boolean | – |
Table.SortButton
The column's label as a control. Its arrow follows the sort of the Table.Head it sits in (up for ascending, down for descending, both ways when unsorted) through CSS, so it needs no client code; the <th>'s aria-sort tells assistive technology the current order.
Server component Renders data-slot="table-sort-button" with the class nz-table-sort-button. Props: TableSortButtonProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
href | string | – | The same page sorted by this column, for example |
render | RenderProp<Empty> | – | The router's link, for example |
- Also takes every attribute of
<button>. (ComponentPropsWithRef<"button">)
render takes an element, or, in client code, a function of the props and no state.
Table.RowLink
A row's link, usually its first cell's text (Figma Order Row: the order number). In a row with interactive, it stretches over the whole row.
Server component Renders data-slot="table-row-link" with the class nz-table-row-link. Props: TableRowLinkProps. Source: src/table/table.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – | The router's link, for example |
- Also takes every attribute of
<a>. (ComponentPropsWithRef<"a">)
render takes an element, or, in client code, a function of the props and no state.
Other exports
TableCaptionPropstypeimport type { TableCaptionProps } from '@nizivo/react/table'The props of
Table.Caption, listed above.TableCellPropstypeimport type { TableCellProps } from '@nizivo/react/table'The props of
Table.Cell, listed above.TableCellStatetypeimport type { TableCellState } from '@nizivo/react/table'The render state of
Table.Cell, listed above.TableContainerPropstypeimport type { TableContainerProps } from '@nizivo/react/table'The props of
Table.Container, listed above.TableHeadPropstypeimport type { TableHeadProps } from '@nizivo/react/table'The props of
Table.Head, listed above.TableHeadStatetypeimport type { TableHeadState } from '@nizivo/react/table'The render state of
Table.Head, listed above.TableRootPropstypeimport type { TableRootProps } from '@nizivo/react/table'The props of
Table.Root, listed above.TableRowLinkPropstypeimport type { TableRowLinkProps } from '@nizivo/react/table'The props of
Table.RowLink, listed above.TableRowPropstypeimport type { TableRowProps } from '@nizivo/react/table'The props of
Table.Row, listed above.TableRowStatetypeimport type { TableRowState } from '@nizivo/react/table'The render state of
Table.Row, listed above.TableSectionPropstypeimport type { TableSectionProps } from '@nizivo/react/table'The props of
Table.Body, listed above.TableSorttypeimport type { TableSort } from '@nizivo/react/table'type TableSort = "ascending" | "descending"TableSortButtonPropstypeimport type { TableSortButtonProps } from '@nizivo/react/table'The props of
Table.SortButton, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
table.nz-table | Table.Rootroot (takes render) | none |
table-body.nz-table-body | Table.Bodyroot (takes render) | none |
table-caption.nz-table-caption | Table.Captionroot (takes render) | none |
table-cell.nz-table-cell | Table.Cellroot (takes render) |
|
table-container.nz-table-container | Table.Containerroot (takes render) | none |
table-footer.nz-table-footer | Table.Footerroot (takes render) | none |
table-head.nz-table-head | Table.Headroot (takes render) |
|
table-header.nz-table-header | Table.Headerroot (takes render) | none |
table-row.nz-table-row | Table.Rowroot (takes render) |
|
table-row-link.nz-table-row-link | Table.RowLinkroot (takes render); interactive, focus: self | none |
table-sort-button.nz-table-sort-button | Table.SortButtonroot (takes render); interactive, focus: self | none |
table-sort-icon.nz-table-sort-icon | Table.SortButtoninner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-table-bg | --nz-bg-surface |
|
--nz-table-border | --nz-border-subtle |
|
--nz-table-cell-padding-block | --nz-space-3 |
|
--nz-table-cell-padding-inline | --nz-space-5 |
|
--nz-table-header-bg | --nz-bg-subtle |
|
--nz-table-radius | --nz-radius-md |
|
--nz-table-row-bg-hover | --nz-bg-hover |
|
--nz-table-row-bg-selected | --nz-bg-brand-subtle |
|
Layers: nz.components. Classes: .nz-inline, .nz-table, .nz-table-body, .nz-table-caption, .nz-table-cell, .nz-table-container, .nz-table-footer, .nz-table-head, .nz-table-row, .nz-table-row-link, .nz-table-select-all, .nz-table-select-row, .nz-table-select-row-input, .nz-table-sort-button, .nz-table-sort-icon.
Tokens this stylesheet reads (23)
--nz-bg-brand-subtle, --nz-bg-hover, --nz-bg-subtle, --nz-bg-surface, --nz-border-focus, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-font-body-m, --nz-font-label-m, --nz-font-label-m-strong, --nz-font-numeric-m, --nz-icon-primary, --nz-icon-secondary, --nz-radius-md, --nz-size-icon-sm, --nz-space-1, --nz-space-3, --nz-space-5, --nz-text-primary, --nz-text-secondary, --nz-tracking-body-m, --nz-tracking-label-m
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
table-row-link: draws the focus outline itself on keyboard focus.table-sort-button: draws the focus outline itself on keyboard focus.
Keyboard
| Keys | Action |
|---|---|
| Tab | Reaches each row's link, then any control in a row, then the sort links in the header, in document order. The table and its cells take no focus. |
| Enter | On a row's link, opens the row; on a sort link, loads the page sorted by that column. |
Source: Native table elements and links; no engine, no grid role. Checked by the browser lane (test/browser/table.spec.tsx) in Chromium, Firefox and WebKit.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: Figma node
7275:14040, Figma node7275:14091 - Ghost masters: Figma node
753:410, Figma node753:476, Figma node753:599, Figma node753:644
TabNav
Links styled as tabs, for views that are URLs (status filters, sub-pages). A navigation landmark of plain links with aria-current="page" on the current one, server-rendered with no JavaScript; a router's link goes in through render. In-page views with panels are Tabs.
TableSelection
Row checkboxes for Table's bulk actions. Native checkboxes rendered on the server submit the checked ids with their form before any JavaScript runs; one small client island adds select-all, Shift-click ranges and Escape.