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
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
columns | TableColumn[] (below) | Yes | N/A | Column definitions |
data | Record<string, ReactNode>[] | Yes | N/A | Row data; a cell can be text, a number or any element, and fields no column shows |
striped | boolean | No | false | Alternate row shading |
bordered | boolean | No | true | Show cell borders |
size | "xs" | "sm" | "md" | No | "md" | Text size and cell padding; the smaller sizes fit more rows |
fit | "content" | "fill" | number | No | "content" | Fill a flex parent or take a height in pixels, and show only the rows that fit it |
rowVariant | (row, index) => "done" | undefined | No | N/A | How each row is set apart; a "done" row's text is struck through, in full ink |
Each column takes:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
key | string | Yes | N/A | The field of each row the column shows |
header | ReactNode | Yes | N/A | The 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 |
truncate | boolean | No | false | Keep the cells on one line ending in "…", taking the room the other columns leave |
highlight | boolean | No | false | Tint the column from its header to its last row, such as today's column in a week |
width | number | "N%" | "min" | No | N/A | Pixels, a share of the table, or "min" for only as wide as its widest cell on one line |
Examples
Simple
Striped
No Border
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.
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"
/>
);
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"
/>
);
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"
/>
);
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.