Table#

Data in real table elements — <table>, <caption>, <thead>, <tbody>, <tfoot>, <tr>, <th>, <td> — wrapped in the scroll container the content's width demands. The elements are the semantics: assistive-technology row and column navigation and header association only exist on a real table, so there is no asChild anywhere in the scope. Zero adds no state, no ids and no ARIA beyond what the elements carry natively.

Import#

TSX
import { Table } from '@sigx/zero/table';

Table is a compound: Table.Root, Table.Caption, Table.Head, Table.Body, Table.Foot, Table.Row, Table.HeaderCell, Table.Cell. It is also re-exported from the @sigx/zero root, together with tableAnatomy.

Usage#

TSX
import { component } from 'sigx';
import { Table } from '@sigx/zero/table';

const Revenue = component(({ signal }) => {
    const state = signal({ selected: 'q2' });
    const rows = [
        { id: 'q1', quarter: 'Q1', revenue: '$12,930' },
        { id: 'q2', quarter: 'Q2', revenue: '$14,210' },
        { id: 'q3', quarter: 'Q3', revenue: '$15,080' },
    ];

    return () => (
        <Table.Root mods={{ zebra: true }}>
            <Table.Caption>Quarterly revenue</Table.Caption>
            <Table.Head>
                <Table.Row>
                    <Table.HeaderCell>Quarter</Table.HeaderCell>
                    <Table.HeaderCell>Revenue</Table.HeaderCell>
                </Table.Row>
            </Table.Head>
            <Table.Body>
                {rows.map((r) => (
                    <Table.Row selected={state.selected === r.id}>
                        <Table.Cell>{r.quarter}</Table.Cell>
                        <Table.Cell>{r.revenue}</Table.Cell>
                    </Table.Row>
                ))}
            </Table.Body>
            <Table.Foot>
                <Table.Row>
                    <Table.HeaderCell scope="row">Total</Table.HeaderCell>
                    <Table.Cell>$42,220</Table.Cell>
                </Table.Row>
            </Table.Foot>
        </Table.Root>
    );
});

Table.Root renders the wrapper div and the <table> inside it; the sections go straight into Root. Write a Caption — it is the table's accessible name, and a data table without one is announced as "table" and nothing more.

Row headers#

TSX
<Table.HeaderCell scope="row">Total</Table.HeaderCell>

HeaderCell renders a <th> whose scope defaults to col. A header that labels its row rather than its column says scope="row", and assistive technology associates the cells accordingly.

Selected rows#

TSX
<Table.Row selected={state.selected === r.id}>

selected renders the shared data-selected flag on the <tr>. It is a per-row fact the app owns — which row is chosen — so it is a flag on the row, not a mod on the table. Zero attaches no behavior to it: the click handling, the checkbox column and the keyboard model of a selectable grid are yours.

A column spec#

TSX
<Table.Root columns={[
    { label: 'Time', width: '8rem' },
    { label: 'What' },
    { key: 'cost', label: 'Cost', align: 'end' },
]}>
    <Table.Caption>Activity</Table.Caption>
    <Table.Head />
    <Table.Body>
        <Table.Row>
            <Table.Cell column={0}>09:12</Table.Cell>
            <Table.Cell column={1}>Deployed</Table.Cell>
            <Table.Cell column="cost">$0.42</Table.Cell>
        </Table.Row>
    </Table.Body>
</Table.Root>

columns on Table.Root describes the columns once — per column an optional label, width (any CSS width, e.g. 8rem, 12ch, 20%; auto when omitted), align ('start' | 'center' | 'end') and key. Table.Head renders the widths as a <colgroup> of <col> parts just before its <thead>, and with no children it renders the header row from the labels. Each <col> carries its width as --table-column-width, which a rule in @sigx/zero/css (@layer zero.structure) applies — a custom property rather than a width literal, so a responsive rule can take it back without !important.

A Table.Cell or Table.HeaderCell names its column by index (column={2}) or by key (column="cost"). It then takes the column's alignment as --table-cell-align, which every shipped design system's cell recipe reads (declared start on the root), and a header cell with no children renders the column's label. Naming a column the spec doesn't have throws.

Attributes, spans and row ids#

TSX
<Table.Row data-row-id={row.id}>…</Table.Row>
<Table.Row><Table.Cell colSpan={5}>No results</Table.Cell></Table.Row>

Every Table part forwards aria-*, your own data-*, id, title and role. Table.Root splits them: aria-* and role go on the <table> — the element assistive technology reads — and the rest on the scroll wrapper. Table.Cell and Table.HeaderCell take the native colSpan and rowSpan.

Zebra striping and hover#

TSX
<Table.Root mods={{ zebra: true, hover: true }}>

Striping and hover-highlight are per-instance styling choices, not anatomy: they render as data-mod-zebra / data-mod-hover on the root, and a design system that offers them draws the stripes and the highlight in CSS. Both shipped design systems do. Under one that does not, the attributes match nothing and the table renders plain.

Anatomy#

PartElementStatesFlagsNotes
rootdiv——The scroll container. Carries the variant axes.
tabletable——The real table. Inside root.
captioncaption——The accessible name. Inside table.
colgroupcolgroup——Rendered by Table.Head from columns, before the <thead>. Inside table.
columncol——One per column; carries --table-column-width. Inside colgroup.
headthead——Inside table.
bodytbody——Inside table.
foottfoot——Inside table.
rowtr—selectedInside whichever section holds it.
header-cellth——scope="col" by default; --table-cell-align when it names a column. Inside row.
celltd——--table-cell-align when it names a column. Inside row.

Every part carries data-scope="table" and data-part="<part>". The root is the scroll container, not the table: a table is the one component whose natural content is wider than its container, and a <table> cannot be its own overflow box — display: table does not scroll — so the wrapper div is anatomy. Recipes give it overflow-x: auto, and the variant axes ride it, where the compiler anchors axis rules. See The anatomy contract.

There are no states — a table has no machine lifecycle — and no sorting: header-cell renders the <th> that would carry aria-sort, so the anatomy is ready for a sortable column without shipping dead parts today.

Props#

Table.Root#

PropTypeDefaultDescription
columnsreadonly TableColumn[]—The column spec: { key?, label?, width?, align? } per column.
color / size / variant / axes / modsdesign-system vocabulary—The variant axes, rendered as data-* on root.
classstring—Extra classes on the root element.
aria-* / roleHTML attributes—Forwarded to the <table>.
data-* / id / titleHTML attributes—Forwarded to the scroll wrapper.

Table.Caption, Table.Head, Table.Body, Table.Foot#

class and the forwarded HTML attributes. Table.Head with no children renders the header row from the columns labels; with columns set it also renders the <colgroup>.

Table.Cell#

PropTypeDefaultDescription
columnnumber | string—The column this cell belongs to, by index or key; takes its alignment. An unknown column throws.
colSpan / rowSpannumber—The native spans.
classstring—Extra classes.

Table.Row#

PropTypeDefaultDescription
selectedbooleanfalseThe app's "this row is chosen"; renders data-selected.
classstring—Extra classes.

Table.HeaderCell#

PropTypeDefaultDescription
scope'col' | 'row''col'Which axis this header labels — the native <th scope>.
columnnumber | string—The column by index or key; takes its alignment, and renders its label when the cell has no children.
colSpan / rowSpannumber—The native spans.
classstring—Extra classes.

Every part also forwards aria-*, data-*, id, title and role.

In the shipped design systems#

colorsizevariantmods
@sigx/zero-basicthe eight recommended rolesxs–xl—zebra · hover
@sigx/zero-daisyuithe eight recommended rolesxs–xl—zebra · hover

Neither wires a variant on the scope; under a design system's /register import the prop is therefore absent, while mods={{ zebra: true }} and mods={{ hover: true }} type-check in both. See Typed vocabulary.

A recipe draws zebra on the even body rows, scoped under [data-mod-zebra] on the root, hover as a row highlight on :hover, and [data-selected] as a stronger tint that wins over both. The size axis scales cell padding and the text token on the cells; color tints the head. Cell and header-cell recipes read text-align: var(--table-cell-align).

Pagination for walking a long table page by page, Skeleton for the rows while they load.