Table
A styled <table>. Compose it from plain semantic rows; the CSS styles the native elements, so no per-cell classes are needed.
Real use · team table
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Grace Hopper | Admin | Active |
| Alan Turing | Engineer | Away |
Stacked in a narrow pane
With stack, a list turns its rows into blocks while its container is narrower than 36rem, so the amount, the state and the button stay in the pane. The same list in a wide pane is an ordinary table. Pass a breakpoint, such as stack="lg", to switch at a different width.
| Customer | Due | State | Actions |
|---|---|---|---|
Harbor Street Bakery INV-1042 | $1,240.00 | Overdue | |
Juniper and Fern Florists INV-1043 | $386.50 | Due | |
Ridgeview Family Dental, second location INV-1044 | $12,900.00 | Paid | |
| Outstanding | $1,626.50 |
| Customer | Due | State | Actions |
|---|---|---|---|
Harbor Street Bakery INV-1042 | $1,240.00 | Overdue | |
Juniper and Fern Florists INV-1043 | $386.50 | Due | |
Ridgeview Family Dental, second location INV-1044 | $12,900.00 | Paid | |
| Outstanding | $1,626.50 |
Stacked with names longer than the pane
A long name truncates inside the pane instead of making the list wider than it. This holds in an app that also uses Tailwind's own table class, which sets the same name to a table display.
| Customer | Due | State | Actions |
|---|---|---|---|
Harbor Street Bakery and Cafe, wholesale bread and pastry accounts for the east side INV-1042 | $1,240.00 | Overdue | |
Juniper and Fern Florists, weddings, events and weekly office arrangements downtown INV-1043 | $386.50 | Due | |
Ridgeview Family Dental, second location, pediatric and orthodontic care on Main Street INV-1044 | $12,900.00 | Paid | |
| Outstanding | $1,626.50 |
Stacked with columns that only show when wide
A cell hidden with hidden (or the hidden attribute) does not count while stacked: the first cell that shows takes its own line, and the last one that shows sits at the end. Here the date sent and the note only appear in a wide pane.
| Sent | Customer | Due | State | Actions | Note |
|---|---|---|---|---|---|
| Sep 2 | Harbor Street Bakery INV-1042 | $1,240.00 | Overdue | Asked for net 30 | |
| Sep 14 | Juniper and Fern Florists INV-1043 | $386.50 | Due | Pays by check | |
| Sep 21 | Ridgeview Family Dental, second location INV-1044 | $12,900.00 | Paid | Paid early |
| Sent | Customer | Due | State | Actions | Note |
|---|---|---|---|---|---|
| Sep 2 | Harbor Street Bakery INV-1042 | $1,240.00 | Overdue | Asked for net 30 | |
| Sep 14 | Juniper and Fern Florists INV-1043 | $386.50 | Due | Pays by check | |
| Sep 21 | Ridgeview Family Dental, second location INV-1044 | $12,900.00 | Paid | Paid early |
Stacked with a checkbox to pick rows
A first cell that holds only a checkbox or radio stays on the same line as the cell after it, which takes the rest of that line. A cell that wraps its control in more markup opts in with data-stack="select".
| Customer | State | Actions | |
|---|---|---|---|
Harbor Street Bakery INV-1042 | Overdue | ||
Juniper and Fern Florists INV-1043 | Due | ||
Ridgeview Family Dental, second location INV-1044 | Paid |
| Customer | State | Actions | |
|---|---|---|---|
Harbor Street Bakery INV-1042 | Overdue | ||
Juniper and Fern Florists INV-1043 | Due | ||
Ridgeview Family Dental, second location INV-1044 | Paid |
Sizes
| Size | Value |
|---|---|
| xs | Row height scales with density |
| Size | Value |
|---|---|
| sm | Row height scales with density |
| Size | Value |
|---|---|
| md | Row height scales with density |
| Size | Value |
|---|---|
| lg | Row height scales with density |
Using Table
Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.
Plain HTML — CSS classes
The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path is complete for Table — it needs no JavaScript, so the classes below are the whole story.
/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";Root class table, with 15 modifiers:
.table.table-hover.table-lg.table-md.table-scroll.table-sm.table-stack.table-stack-2xl.table-stack-3xl.table-stack-lg.table-stack-md.table-stack-sm.table-stack-xl.table-xl.table-xs.table-zebra
React
Typed components over those same classes, with behavior and accessibility from Base UI.
npm i @wizeworks/silicaui-reactimport { Table } from "@wizeworks/silicaui-react";Props
| Prop | Type | Required | Description |
|---|---|---|---|
| zebra | boolean | no | Striped rows. |
| hover | boolean | no | Highlight the row under the cursor. |
| size | TableSize | no | Default md. Scales cell padding + type. |
| stack | boolean | TableStackBreakpoint | no | Stack the rows into blocks while the table's container is narrow, so a list fits a narrow pane instead of running past it. |
| wrapperClassName | string | no | Class for the wrapper <div>. It scrolls a wide table sideways, and it is the container a stacked table measures. |
Source of the demo above
The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.
Show source
import { Table, Badge, Button, Checkbox } from "@wizeworks/silicaui-react";
import { Section } from "../lib/Section";
const ROWS = [
{ name: "Ada Lovelace", role: "Engineer", status: "Active" as const },
{ name: "Grace Hopper", role: "Admin", status: "Active" as const },
{ name: "Alan Turing", role: "Engineer", status: "Away" as const },
];
const INVOICES = [
{ customer: "Harbor Street Bakery", number: "INV-1042", due: "$1,240.00", state: "Overdue" as const, sent: "Sep 2", note: "Asked for net 30" },
{ customer: "Juniper and Fern Florists", number: "INV-1043", due: "$386.50", state: "Due" as const, sent: "Sep 14", note: "Pays by check" },
{ customer: "Ridgeview Family Dental, second location", number: "INV-1044", due: "$12,900.00", state: "Paid" as const, sent: "Sep 21", note: "Paid early" },
];
/**
* The same invoices with customer names longer than a narrow pane is wide. A
* truncated name takes no room in a block, but a table grows to fit its
* longest unbroken line, so these only fit a 343px pane if the stacked table
* is really a block.
*/
const LONG_NAMES = [
"Harbor Street Bakery and Cafe, wholesale bread and pastry accounts for the east side",
"Juniper and Fern Florists, weddings, events and weekly office arrangements downtown",
"Ridgeview Family Dental, second location, pediatric and orthodontic care on Main Street",
];
const LONG_INVOICES = INVOICES.map((r, i) => ({ ...r, customer: LONG_NAMES[i] ?? r.customer }));
const STATE_COLOR = { Overdue: "error", Due: "warning", Paid: "success" } as const;
/**
* A list shaped the way real list panes are: a name cell that fills the row
* and truncates, then nowrap cells for the amount, the state, and the row's own
* button. As a table that is wider than a 343px pane; stacked, it fits.
*/
function InvoiceList({ rows = INVOICES }: { rows?: typeof INVOICES }) {
return (
<Table stack zebra size="sm">
<thead>
<tr>
<th>Customer</th>
<th>Due</th>
<th>State</th>
<th>
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.number}>
<td className="w-full max-w-0 min-w-56">
<div className="truncate font-semibold">{r.customer}</div>
<div className="truncate">{r.number}</div>
</td>
<td data-label="Due" className="whitespace-nowrap">
{r.due}
</td>
<td className="whitespace-nowrap">
<Badge color={STATE_COLOR[r.state]} variant="soft">
{r.state}
</Badge>
</td>
<td className="whitespace-nowrap text-right">
<Button size="sm">Send reminder</Button>
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td>Outstanding</td>
<td className="whitespace-nowrap">$1,626.50</td>
<td />
<td />
</tr>
</tfoot>
</Table>
);
}
/**
* The same list with two columns that only show in a wide pane: when it was
* sent, ahead of the customer, and a note after the button. Both carry Tailwind's
* `hidden` plus a container variant, so in a narrow pane the row's first and last
* cells are hidden ones. Stacked, the customer still gets its own line and the
* button still sits at the end, because the stack skips hidden cells.
*/
function SentInvoiceList() {
return (
<Table stack size="sm">
<thead>
<tr>
<th className="hidden @xl:table-cell">Sent</th>
<th>Customer</th>
<th>Due</th>
<th>State</th>
<th>
<span className="sr-only">Actions</span>
</th>
<th className="hidden @xl:table-cell">Note</th>
</tr>
</thead>
<tbody>
{INVOICES.map((r) => (
<tr key={r.number}>
<td className="hidden whitespace-nowrap @xl:table-cell">{r.sent}</td>
<td className="w-full max-w-0 min-w-56">
<div className="truncate font-semibold">{r.customer}</div>
<div className="truncate">{r.number}</div>
</td>
<td data-label="Due" className="whitespace-nowrap">
{r.due}
</td>
<td className="whitespace-nowrap">
<Badge color={STATE_COLOR[r.state]} variant="soft">
{r.state}
</Badge>
</td>
<td className="whitespace-nowrap text-right">
<Button size="sm">Send reminder</Button>
</td>
<td className="hidden whitespace-nowrap @xl:table-cell">{r.note}</td>
</tr>
))}
</tbody>
</Table>
);
}
/**
* A list whose rows can be picked for a bulk action: the first cell holds only a
* checkbox. Stacked, the checkbox stays on the customer's line instead of taking
* a line of its own. With `hideSelect`, the checkbox cell is hidden below a 24rem
* pane (`hidden @sm:table-cell`), and the customer gets its own line as usual.
*/
function SelectableInvoiceList({ hideSelect = false }: { hideSelect?: boolean }) {
const selectCell = hideSelect ? "hidden w-10 @sm:table-cell" : "w-10";
return (
<Table stack size="sm">
<thead>
<tr>
<th className={selectCell}>
<Checkbox aria-label="Pick every invoice" />
</th>
<th>Customer</th>
<th>State</th>
<th>
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{INVOICES.map((r) => (
<tr key={r.number}>
<td className={selectCell}>
<Checkbox aria-label={`Pick ${r.number}`} />
</td>
<td className="w-full max-w-0 min-w-56">
<div className="truncate font-semibold">{r.customer}</div>
<div className="truncate">{r.number}</div>
</td>
<td className="whitespace-nowrap">
<Badge color={STATE_COLOR[r.state]} variant="soft">
{r.state}
</Badge>
</td>
<td className="whitespace-nowrap text-right">
<Button size="sm">Send reminder</Button>
</td>
</tr>
))}
</tbody>
</Table>
);
}
export function TableDemo() {
return (
<>
<Section title="Real use · team table">
<Table zebra hover className="max-w-xl">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{ROWS.map((r) => (
<tr key={r.name}>
<td>{r.name}</td>
<td>{r.role}</td>
<td>
<Badge
color={r.status === "Active" ? "success" : "warning"}
variant="soft"
>
{r.status}
</Badge>
</td>
</tr>
))}
</tbody>
</Table>
</Section>
<Section title="Stacked in a narrow pane">
<p className="max-w-xl">
With <code>stack</code>, a list turns its rows into blocks while its
container is narrower than 36rem, so the amount, the state and the
button stay in the pane. The same list in a wide pane is an ordinary
table. Pass a breakpoint, such as <code>stack="lg"</code>, to switch at
a different width.
</p>
<div className="flex flex-col gap-6">
<div
data-demo="stack-narrow"
className="w-[343px] max-w-full rounded-box border border-base-300"
>
<InvoiceList />
</div>
<div data-demo="stack-wide" className="rounded-box border border-base-300">
<InvoiceList />
</div>
</div>
</Section>
<Section title="Stacked with names longer than the pane">
<p className="max-w-xl">
A long name truncates inside the pane instead of making the list wider
than it. This holds in an app that also uses Tailwind's own{" "}
<code>table</code> class, which sets the same name to a table display.
</p>
<div
data-demo="stack-long-narrow"
className="w-[343px] max-w-full rounded-box border border-base-300"
>
<InvoiceList rows={LONG_INVOICES} />
</div>
</Section>
<Section title="Stacked with columns that only show when wide">
<p className="max-w-xl">
A cell hidden with <code>hidden</code> (or the <code>hidden</code>{" "}
attribute) does not count while stacked: the first cell that shows
takes its own line, and the last one that shows sits at the end. Here
the date sent and the note only appear in a wide pane.
</p>
<div className="flex flex-col gap-6">
<div
data-demo="stack-hidden-narrow"
className="w-[343px] max-w-full rounded-box border border-base-300"
>
<SentInvoiceList />
</div>
<div data-demo="stack-hidden-wide" className="rounded-box border border-base-300">
<SentInvoiceList />
</div>
</div>
</Section>
<Section title="Stacked with a checkbox to pick rows">
<p className="max-w-xl">
A first cell that holds only a checkbox or radio stays on the same line
as the cell after it, which takes the rest of that line. A cell that
wraps its control in more markup opts in with{" "}
<code>data-stack="select"</code>.
</p>
<div className="flex flex-col gap-6">
<div
data-demo="stack-select-narrow"
className="w-[343px] max-w-full rounded-box border border-base-300"
>
<SelectableInvoiceList />
</div>
<div
data-demo="stack-select-hidden-narrow"
className="w-[343px] max-w-full rounded-box border border-base-300"
>
<SelectableInvoiceList hideSelect />
</div>
</div>
</Section>
<Section title="Sizes">
<div className="flex max-w-xl flex-col gap-4">
{(["xs", "sm", "md", "lg"] as const).map((size) => (
<Table key={size} size={size} zebra>
<thead>
<tr>
<th>Size</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>{size}</td>
<td>Row height scales with density</td>
</tr>
</tbody>
</Table>
))}
</div>
</Section>
</>
);
}Vanilla — node tree to HTML
Table lowers to <table>, holding its thead/tbody/tfoot children. Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.
npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviorsimport { atom, toHtml } from "@wizeworks/silicaui-html";
const node = atom("Table", "table", { /* props */ }, [/* children */]);
const markup = toHtml(node);Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "table table-primary" rather than passing a colour as a prop.
- Takes children
- yes
- Hydrated by
- nothing — this one is static markup
The full API for Table, on every path, is available through the Silica MCP server.