Skip to main content

Table

Every example on this page is live, drawn by the library's Storybook, where you can also change its props and see the result.

An HTML table with configurable borders, striping, and column alignment. A cell or a header can hold any element, such as a Badge. Set size for denser rows, truncate on a column to keep its cells on one line, width and highlight to shape a grid, rowVariant to strike finished rows through, and fit to show only the rows a box holds.

Import​

import { Table } from "~lib/data-display/table";

Props​

NameTypeRequiredDefaultDescription
columnsTableColumn[] (below)YesN/AColumn definitions
dataRecord<string, ReactNode>[]YesN/ARow data; a cell can be text, a number or any element, and fields no column shows
stripedbooleanNofalseAlternate row shading
borderedbooleanNotrueShow cell borders
size"xs" | "sm" | "md"No"md"Text size and cell padding; the smaller sizes fit more rows
fit"content" | "fill" | numberNo"content"Fill a flex parent or take a height in pixels, and show only the rows that fit it
rowVariant(row, index) => "done" | undefinedNoN/AHow each row is set apart; a "done" row's text is struck through, in full ink

Each column takes:

NameTypeRequiredDefaultDescription
keystringYesN/AThe field of each row the column shows
headerReactNodeYesN/AThe column's heading: text, or any element, such as a day with a badge
align"left" | "center" | "right"No"left"Text alignment of the header and the cells
truncatebooleanNofalseKeep the cells on one line ending in "…", taking the room the other columns leave
highlightbooleanNofalseTint the column from its header to its last row, such as today's column in a week
widthnumber | "N%" | "min"NoN/APixels, a share of the table, or "min" for only as wide as its widest cell on one line

Examples​

Simple​

Open Simple in Storybook

Striped​

Open Striped in Storybook

No Border​

Open No Border in Storybook

Sizes​

size sets the text and the cell padding together. "sm" keeps the text and tightens the padding; "xs" makes both smaller, for a box that needs as many rows as it can hold.

Open Sizes in Storybook

Truncated Columns​

A column with truncate keeps its cells on one line, ending in "…" when the text is too long. It takes the room the other columns leave, and those keep to one line at their natural width, so every row stays the same height. A cell can hold any element, such as a badge for a train's line.

import { Badge } from "~lib/data-display/badge";
import { Table } from "~lib/data-display/table";

const Departures = ({
trains,
}: {
trains: { line: string; destination: string; due: string }[];
}) => (
<Table
columns={[
{ key: "line", header: "Line" },
{ key: "destination", header: "Destination", truncate: true },
{ key: "due", header: "Due", align: "right" },
]}
data={trains.map((train) => ({
...train,
line: <Badge size="sm">{train.line}</Badge>,
}))}
size="sm"
fit="fill"
/>
);
Open Truncated in Storybook

Done Rows​

rowVariant is given each row and its index and names how the row is set apart. A "done" row's text is struck through in full ink, so a list of tasks reads as a checklist. A field no column shows, such as done, never draws, so the row can carry what rowVariant reads. An element in a done row, such as a badge, is drawn as it is.

import { Table } from "~lib/data-display/table";

const Tasks = ({
tasks,
}: {
tasks: { content: string; due: string; checked: boolean }[];
}) => (
<Table
columns={[
{ key: "task", header: "Task", truncate: true },
{ key: "due", header: "Due", align: "right" },
]}
data={tasks.map((task) => ({
task: task.content,
due: task.due,
done: task.checked,
}))}
rowVariant={(row) => (row.done ? "done" : undefined)}
size="sm"
/>
);
Open Checklist in Storybook

A Week Grid​

highlight tints a column from its header to its last row, so the eye lands on today in a week. A header can be any element, so today's date can sit on a solid circle the way a calendar marks it. width sizes a column: a number of pixels, a share such as "16%", or "min" for only as wide as its widest cell on one line. Give the row names "min" and the days equal shares that together leave the names their room, and the days stay even whatever they hold. Shares that add up to more than the table are scaled back unevenly, from the content.

import { Badge } from "~lib/data-display/badge";
import { Table } from "~lib/data-display/table";

const Week = ({
days,
today,
slots,
}: {
days: { key: string; name: string; date: number }[];
today: string;
slots: { name: string; meals: Record<string, string> }[];
}) => (
<Table
columns={[
{ key: "slot", header: "", width: "min" },
...days.map((day) => ({
key: day.key,
header:
day.key === today ? (
<span className="inline-flex items-center gap-1">
{day.name}
<Badge size="xs" shape="circle">
{day.date}
</Badge>
</span>
) : (
`${day.name} ${day.date}`
),
highlight: day.key === today,
width: `${80 / days.length}%` as const,
})),
]}
data={slots.map((slot) => ({ slot: slot.name, ...slot.meals }))}
size="sm"
/>
);
Open Week Grid in Storybook

Fill a Box​

With fit="fill" the table takes the room its flex parent leaves and shows as many whole rows as fit below its header, so a widget does not have to guess how many rows its box holds. A number takes that height in pixels instead.

Open Fill Box in Storybook

See Also​