Table

Composable primitives for tabular data. The shell is bordered and scrolls on overflow, the header speaks in the label voice, and every surface reads from tokens.

import from ~/components/ui/table

Open positions

Open positions at 30 June 2026. Weight is percent of net asset value.
PositionSectorStateWeightReturn
ASML HoldingSemiconductorsOpen9.4%+18.2%
Constellation SoftwareSoftwareOpen7.1%+6.5%
Brookfield CorpFinancialsTrimming5.8%-3.4%
Taiwan SemiconductorSemiconductorsOpen4.2%+11.9%
EnbridgeEnergyExiting2.6%-1.1%
Allocated29.1%+6.4%

header, body, footer, caption

A caption names the table for everyone. Header cells carry scope="col" so a screen reader can say which column a value belongs to.

Empty table

Open positions matching the current filters.
PositionSectorStateWeight

No positions match these filters

Widen the sector filter or clear the search to see the rest of the book.

filtered to nothing

Table has no empty prop, so the call site fills one full-width row. The header stays put, which tells the user the columns are still there and the filter is what emptied them.

Rules

Do

Use for tabular data: comparable rows and a stable set of columns.
Give every table a TableCaption. It names the table and its as-of date for everyone reading it.
Put scope="col" on every TableHead so assistive tech can pair a value with its column.
Right-align numeric columns and give them tabular figures so the digits line up down the column.
Compose the whole tree in one React file. Split across islands, the parts never meet.

Don't

Do not use a table for layout, or for a handful of key/value pairs. That is a definition list.
Do not ship a header-only table when the rows are empty. Fill one full-width row with an EmptyState.
Do not pass color, font or border classes to a cell. Alignment and column width are the only call-site business.