Tables
Native <table> semantics. Cells inherit style from descendant selectors — no per-cell class. Modifiers (striped, bordered, relaxed, sticky) compose freely. Default row height is ~32px; use relaxed for lighter layouts.
Examples
Section titled “Examples”A bare .table styles <thead>, <tbody>, <tr>, <th>, and <td>. The React API mirrors the HTML via dot-notation subparts.
| Name | Role | |
|---|---|---|
| Ada Lovelace | ada@example.com | Admin |
| Grace Hopper | grace@example.com | Editor |
| Alan Turing | alan@example.com | Viewer |
| Name | Role | |
|---|---|---|
| Ada Lovelace | ada@example.com | Admin |
| Grace Hopper | grace@example.com | Editor |
| Alan Turing | alan@example.com | Viewer |
<table class="table"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Role</th> </tr> </thead> <tbody> <tr> <td>Ada Lovelace</td> <td>ada@example.com</td> <td>Admin</td> </tr> <tr> <td>Grace Hopper</td> <td>grace@example.com</td> <td>Editor</td> </tr> <tr> <td>Alan Turing</td> <td>alan@example.com</td> <td>Viewer</td> </tr> </tbody></table><Table> <Table.Head> <Table.Row> <Table.HeaderCell>Name</Table.HeaderCell> <Table.HeaderCell>Email</Table.HeaderCell> <Table.HeaderCell>Role</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell>Ada Lovelace</Table.Cell> <Table.Cell>ada@example.com</Table.Cell> <Table.Cell>Admin</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>Grace Hopper</Table.Cell> <Table.Cell>grace@example.com</Table.Cell> <Table.Cell>Editor</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>Alan Turing</Table.Cell> <Table.Cell>alan@example.com</Table.Cell> <Table.Cell>Viewer</Table.Cell> </Table.Row> </Table.Body></Table>Modifiers
Section titled “Modifiers”striped zebra-stripes the body, bordered draws cell borders, relaxed increases padding, sticky pins the header (requires a scrolling ancestor — see below).
| Order | Status |
|---|---|
| #1001 | Shipped |
| #1002 | Processing |
| #1003 | Shipped |
| #1004 | Processing |
| Order | Status |
|---|---|
| #1001 | Shipped |
| #1002 | Processing |
| #1003 | Shipped |
| #1004 | Processing |
<table class="table table-striped"> <thead> <tr> <th>Order</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td>#1001</td> <td>Shipped</td> </tr> <tr> <td>#1002</td> <td>Processing</td> </tr> <tr> <td>#1003</td> <td>Shipped</td> </tr> <tr> <td>#1004</td> <td>Processing</td> </tr> </tbody></table><Table striped> <Table.Head> <Table.Row> <Table.HeaderCell>Order</Table.HeaderCell> <Table.HeaderCell>Status</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell>#1001</Table.Cell> <Table.Cell>Shipped</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>#1002</Table.Cell> <Table.Cell>Processing</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>#1003</Table.Cell> <Table.Cell>Shipped</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>#1004</Table.Cell> <Table.Cell>Processing</Table.Cell> </Table.Row> </Table.Body></Table>| SKU | Name |
|---|---|
| A-001 | Widget |
| A-002 | Gadget |
| SKU | Name |
|---|---|
| A-001 | Widget |
| A-002 | Gadget |
<table class="table table-bordered table-relaxed"> <thead> <tr> <th>SKU</th> <th>Name</th> </tr> </thead> <tbody> <tr> <td>A-001</td> <td>Widget</td> </tr> <tr> <td>A-002</td> <td>Gadget</td> </tr> </tbody></table><Table bordered relaxed> <Table.Head> <Table.Row> <Table.HeaderCell>SKU</Table.HeaderCell> <Table.HeaderCell>Name</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell>A-001</Table.Cell> <Table.Cell>Widget</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>A-002</Table.Cell> <Table.Cell>Gadget</Table.Cell> </Table.Row> </Table.Body></Table>Sticky header
Section titled “Sticky header”sticky pins the header while the body scrolls. The table doesn’t own the scroll region — wrap it in an overflowing container.
| ID | Customer |
|---|---|
| #1001 | Ada |
| #1002 | Grace |
| #1003 | Alan |
| #1004 | Donald |
| #1005 | Edsger |
| #1006 | Linus |
| #1007 | Tony |
| #1008 | Margaret |
| #1009 | Barbara |
| #1010 | Frances |
| ID | Customer |
|---|---|
| #1001 | Ada |
| #1002 | Grace |
| #1003 | Alan |
| #1004 | Donald |
| #1005 | Edsger |
| #1006 | Linus |
| #1007 | Tony |
| #1008 | Margaret |
| #1009 | Barbara |
| #1010 | Frances |
<div style="overflow: auto; max-height: 240px"> <table class="table table-sticky"> <thead> <tr> <th>ID</th> <th>Customer</th> </tr> </thead> <tbody> <tr> <td>#1001</td> <td>Ada</td> </tr> <tr> <td>#1002</td> <td>Grace</td> </tr> <tr> <td>#1003</td> <td>Alan</td> </tr> <tr> <td>#1004</td> <td>Donald</td> </tr> <tr> <td>#1005</td> <td>Edsger</td> </tr> <tr> <td>#1006</td> <td>Linus</td> </tr> <tr> <td>#1007</td> <td>Tony</td> </tr> <tr> <td>#1008</td> <td>Margaret</td> </tr> <tr> <td>#1009</td> <td>Barbara</td> </tr> <tr> <td>#1010</td> <td>Frances</td> </tr> </tbody> </table></div><div style={{ overflow: "auto", maxHeight: 240 }}> <Table sticky> <Table.Head> <Table.Row> <Table.HeaderCell>ID</Table.HeaderCell> <Table.HeaderCell>Customer</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> {[ ["#1001", "Ada"], ["#1002", "Grace"], ["#1003", "Alan"], ["#1004", "Donald"], ["#1005", "Edsger"], ["#1006", "Linus"], ["#1007", "Tony"], ["#1008", "Margaret"], ["#1009", "Barbara"], ["#1010", "Frances"], ].map(([id, name]) => ( <Table.Row key={id}> <Table.Cell>{id}</Table.Cell> <Table.Cell>{name}</Table.Cell> </Table.Row> ))} </Table.Body> </Table></div>Cell alignment
Section titled “Cell alignment”Set data-align="right" (or "center") on <td>/<th> — or pass align to <Table.Cell> / <Table.HeaderCell>. Header and body should agree. The numeric modifier also turns on tabular-nums so currency digits don’t shimmy.
| Item | Qty | Total |
|---|---|---|
| Widget | 3 | $129.00 |
| Gadget | 12 | $1,344.50 |
| Item | Qty | Total |
|---|---|---|
| Widget | 3 | $129.00 |
| Gadget | 12 | $1,344.50 |
<table class="table"> <thead> <tr> <th>Item</th> <th data-align="right">Qty</th> <th data-align="right">Total</th> </tr> </thead> <tbody> <tr> <td>Widget</td> <td data-align="right">3</td> <td data-align="right">$129.00</td> </tr> <tr> <td>Gadget</td> <td data-align="right">12</td> <td data-align="right">$1,344.50</td> </tr> </tbody></table><Table> <Table.Head> <Table.Row> <Table.HeaderCell>Item</Table.HeaderCell> <Table.HeaderCell align="right">Qty</Table.HeaderCell> <Table.HeaderCell align="right">Total</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell>Widget</Table.Cell> <Table.Cell numeric>3</Table.Cell> <Table.Cell numeric>$129.00</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>Gadget</Table.Cell> <Table.Cell numeric>12</Table.Cell> <Table.Cell numeric>$1,344.50</Table.Cell> </Table.Row> </Table.Body></Table>Status gutter
Section titled “Status gutter”A narrow leading cell for row-level status glyphs. .table-cell-gutter (or gutter prop) centers the icon and uses muted text; colour the icon explicitly when status carries meaning.
| Order | Customer | Total | |
|---|---|---|---|
| #1001 | Ada Lovelace | $129.00 | |
| #1002 | Grace Hopper | $72.50 | |
| #1003 | Alan Turing | $310.00 |
| Order | Customer | Total | |
|---|---|---|---|
| #1001 | Ada Lovelace | $129.00 | |
| #1002 | Grace Hopper | $72.50 | |
| #1003 | Alan Turing | $310.00 |
<table class="table"> <thead> <tr> <th class="table-cell-gutter"></th> <th>Order</th> <th>Customer</th> <th data-align="right">Total</th> </tr> </thead> <tbody> <tr> <td class="table-cell-gutter"> <i class="ti ti-circle-check" style="color: var(--color-success)" aria-hidden="true" ></i> </td> <td>#1001</td> <td>Ada Lovelace</td> <td data-align="right">$129.00</td> </tr> <tr> <td class="table-cell-gutter"> <i class="ti ti-clock" aria-hidden="true"></i> </td> <td>#1002</td> <td>Grace Hopper</td> <td data-align="right">$72.50</td> </tr> <tr> <td class="table-cell-gutter"> <i class="ti ti-circle-x" style="color: var(--color-danger)" aria-hidden="true" ></i> </td> <td>#1003</td> <td>Alan Turing</td> <td data-align="right">$310.00</td> </tr> </tbody></table><Table> <Table.Head> <Table.Row> <Table.HeaderCell gutter /> <Table.HeaderCell>Order</Table.HeaderCell> <Table.HeaderCell>Customer</Table.HeaderCell> <Table.HeaderCell align="right">Total</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell gutter> <IconCircleCheck size={16} style={{ color: "var(--color-success)" }} aria-hidden /> </Table.Cell> <Table.Cell>#1001</Table.Cell> <Table.Cell>Ada Lovelace</Table.Cell> <Table.Cell numeric>$129.00</Table.Cell> </Table.Row> <Table.Row> <Table.Cell gutter> <IconClock size={16} aria-hidden /> </Table.Cell> <Table.Cell>#1002</Table.Cell> <Table.Cell>Grace Hopper</Table.Cell> <Table.Cell numeric>$72.50</Table.Cell> </Table.Row> <Table.Row> <Table.Cell gutter> <IconCircleX size={16} style={{ color: "var(--color-danger)" }} aria-hidden /> </Table.Cell> <Table.Cell>#1003</Table.Cell> <Table.Cell>Alan Turing</Table.Cell> <Table.Cell numeric>$310.00</Table.Cell> </Table.Row> </Table.Body></Table>Row selection
Section titled “Row selection”Put a Checkbox in the first cell — row tint is driven by :has(input:checked), no state mirror needed. Wire the select-all header checkbox yourself.
| Order | Customer | |
|---|---|---|
| #1001 | Ada Lovelace | |
| #1002 | Grace Hopper | |
| #1003 | Alan Turing |
| Order | Customer | |
|---|---|---|
| #1001 | Ada Lovelace | |
| #1002 | Grace Hopper | |
| #1003 | Alan Turing |
<table class="table"> <thead> <tr> <th class="table-cell-gutter"> <input type="checkbox" class="checkbox" aria-label="Select all" /> </th> <th>Order</th> <th>Customer</th> </tr> </thead> <tbody> <tr> <td class="table-cell-gutter"> <input type="checkbox" class="checkbox" aria-label="Select #1001" /> </td> <td>#1001</td> <td>Ada Lovelace</td> </tr> <tr> <td class="table-cell-gutter"> <input type="checkbox" class="checkbox" aria-label="Select #1002" checked /> </td> <td>#1002</td> <td>Grace Hopper</td> </tr> <tr> <td class="table-cell-gutter"> <input type="checkbox" class="checkbox" aria-label="Select #1003" /> </td> <td>#1003</td> <td>Alan Turing</td> </tr> </tbody></table><Table> <Table.Head> <Table.Row> <Table.HeaderCell gutter> <Checkbox aria-label="Select all" /> </Table.HeaderCell> <Table.HeaderCell>Order</Table.HeaderCell> <Table.HeaderCell>Customer</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell gutter> <Checkbox aria-label="Select #1001" /> </Table.Cell> <Table.Cell>#1001</Table.Cell> <Table.Cell>Ada Lovelace</Table.Cell> </Table.Row> <Table.Row> <Table.Cell gutter> <Checkbox aria-label="Select #1002" defaultChecked /> </Table.Cell> <Table.Cell>#1002</Table.Cell> <Table.Cell>Grace Hopper</Table.Cell> </Table.Row> <Table.Row> <Table.Cell gutter> <Checkbox aria-label="Select #1003" /> </Table.Cell> <Table.Cell>#1003</Table.Cell> <Table.Cell>Alan Turing</Table.Cell> </Table.Row> </Table.Body></Table>For programmatic selection without a checkbox, use <Table.Row selected> — it sets data-selected and applies the same tint.
| #1001 | Ada |
| #1002 | Grace |
| #1003 | Alan |
| #1001 | Ada |
| #1002 | Grace |
| #1003 | Alan |
<table class="table"> <tbody> <tr> <td>#1001</td> <td>Ada</td> </tr> <tr data-selected> <td>#1002</td> <td>Grace</td> </tr> <tr> <td>#1003</td> <td>Alan</td> </tr> </tbody></table><Table> <Table.Body> <Table.Row> <Table.Cell>#1001</Table.Cell> <Table.Cell>Ada</Table.Cell> </Table.Row> <Table.Row selected> <Table.Cell>#1002</Table.Cell> <Table.Cell>Grace</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>#1003</Table.Cell> <Table.Cell>Alan</Table.Cell> </Table.Row> </Table.Body></Table>Whole-row link
Section titled “Whole-row link”Add .table-row-link to the <tr> (or asLink on <Table.Row>) and put the link in the first cell. CSS expands the anchor’s hit area via ::after, so the whole row is clickable and keyboard-focusable through the real <a>. Other in-row buttons and links stay clickable because they sit above the pseudo-element.
<table class="table"> <thead> <tr> <th>Order</th> <th>Customer</th> <th data-align="right">Total</th> </tr> </thead> <tbody> <tr class="table-row-link"> <td><a href="#1001">#1001</a></td> <td>Ada Lovelace</td> <td data-align="right">$129.00</td> </tr> <tr class="table-row-link"> <td><a href="#1002">#1002</a></td> <td>Grace Hopper</td> <td data-align="right">$72.50</td> </tr> <tr class="table-row-link"> <td><a href="#1003">#1003</a></td> <td>Alan Turing</td> <td data-align="right">$310.00</td> </tr> </tbody></table><Table> <Table.Head> <Table.Row> <Table.HeaderCell>Order</Table.HeaderCell> <Table.HeaderCell>Customer</Table.HeaderCell> <Table.HeaderCell align="right">Total</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row asLink> <Table.Cell> <a href="#1001">#1001</a> </Table.Cell> <Table.Cell>Ada Lovelace</Table.Cell> <Table.Cell numeric>$129.00</Table.Cell> </Table.Row> <Table.Row asLink> <Table.Cell> <a href="#1002">#1002</a> </Table.Cell> <Table.Cell>Grace Hopper</Table.Cell> <Table.Cell numeric>$72.50</Table.Cell> </Table.Row> <Table.Row asLink> <Table.Cell> <a href="#1003">#1003</a> </Table.Cell> <Table.Cell>Alan Turing</Table.Cell> <Table.Cell numeric>$310.00</Table.Cell> </Table.Row> </Table.Body></Table>Footer row
Section titled “Footer row”<Table.Foot> (or <tfoot>) is unstyled — class it yourself. Common use: a totals row mirroring the body’s column alignment.
| Item | Total |
|---|---|
| Widget | $129.00 |
| Gadget | $72.50 |
| Total | $201.50 |
| Item | Total |
|---|---|
| Widget | $129.00 |
| Gadget | $72.50 |
| Total | $201.50 |
<table class="table"> <thead> <tr> <th>Item</th> <th data-align="right">Total</th> </tr> </thead> <tbody> <tr> <td>Widget</td> <td data-align="right">$129.00</td> </tr> <tr> <td>Gadget</td> <td data-align="right">$72.50</td> </tr> </tbody> <tfoot> <tr> <td><strong>Total</strong></td> <td data-align="right"><strong>$201.50</strong></td> </tr> </tfoot></table><Table> <Table.Head> <Table.Row> <Table.HeaderCell>Item</Table.HeaderCell> <Table.HeaderCell align="right">Total</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell>Widget</Table.Cell> <Table.Cell numeric>$129.00</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>Gadget</Table.Cell> <Table.Cell numeric>$72.50</Table.Cell> </Table.Row> </Table.Body> <Table.Foot> <Table.Row> <Table.Cell> <strong>Total</strong> </Table.Cell> <Table.Cell numeric> <strong>$201.50</strong> </Table.Cell> </Table.Row> </Table.Foot></Table>Horizontal overflow
Section titled “Horizontal overflow”<Table> doesn’t wrap itself in a scroll container. Wrap explicitly when you need horizontal scroll on narrow viewports:
<div style="overflow-x: auto"> <table class="table"> … </table></div>