Table
Data table with variants and overflow helper
Intent
Table is a semantic wrapper: .table on the
<table> element, all styling on semantic
descendants (thead, tbody,
th, td). Variants layer on top for
visual distinction -- striping, borders, density, hover -- and
they compose. Helpers round the component out:
.table-wrap for responsive horizontal overflow,
and .as-num for right-aligned tabular numbers.
Basic usage
.table on <table>, inside a
<div class="table-wrap">. The wrap is
strongly recommended even for tables that fit today -- content
grows, column counts shift, and the wrap gracefully hands off
to horizontal scroll when a table outgrows its container.
| Name | Role | |
|---|---|---|
| Ada Lovelace | ada@example.com | Admin |
| Grace Hopper | grace@example.com | Editor |
| Katherine Johnson | katherine@example.com | Viewer |
<docs-resize>
<div class="table-wrap">
<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>Katherine Johnson</td>
<td>katherine@example.com</td>
<td>Viewer</td>
</tr>
</tbody>
</table>
</div>
</docs-resize>Variants
Striped
.is-striped paints alternating tbody rows with a
sunken surface tone. Useful when rows carry enough content that
the eye needs help tracking across wide rows.
| Project | Owner | Status |
|---|---|---|
| Atlas migration | Ada Lovelace | In progress |
| Billing refresh | Grace Hopper | Blocked |
| Onboarding rewrite | Katherine Johnson | In review |
| SSO integration | Hedy Lamarr | Done |
<docs-resize>
<div class="table-wrap">
<table class="table is-striped">
<thead>
<tr>
<th>Project</th>
<th>Owner</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr><td>Atlas migration</td><td>Ada Lovelace</td><td>In progress</td></tr>
<tr><td>Billing refresh</td><td>Grace Hopper</td><td>Blocked</td></tr>
<tr><td>Onboarding rewrite</td><td>Katherine Johnson</td><td>In review</td></tr>
<tr><td>SSO integration</td><td>Hedy Lamarr</td><td>Done</td></tr>
</tbody>
</table>
</div>
</docs-resize>Bordered
.is-bordered draws borders around every cell and
the outer table. Inside a .table-wrap.is-bordered
the table's outer border is suppressed so it doesn't double up
with the wrap's frame; in a default (unframed) wrap the
table's outer border shows.
| Environment | Region | Version |
|---|---|---|
| Production | us-east-1 | 3.14.2 |
| Staging | us-east-1 | 3.15.0-rc.2 |
| Development | local | main@HEAD |
<docs-resize>
<div class="table-wrap">
<table class="table is-bordered">
<thead>
<tr>
<th>Environment</th>
<th>Region</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr><td>Production</td><td>us-east-1</td><td>3.14.2</td></tr>
<tr><td>Staging</td><td>us-east-1</td><td>3.15.0-rc.2</td></tr>
<tr><td>Development</td><td>local</td><td>main@HEAD</td></tr>
</tbody>
</table>
</div>
</docs-resize>Compact
.is-compact tightens cell padding one density tier
and drops the font size one step. Pick it for dense data
tables, long listings, admin views where scan speed beats
breathing room.
| SKU | Product | Warehouse | In stock |
|---|---|---|---|
| W-1042 | Widget, small | Columbus | 238 |
| W-1043 | Widget, medium | Columbus | 117 |
| W-1044 | Widget, large | Columbus | 42 |
| G-2201 | Gadget, basic | Reno | 1,204 |
| G-2202 | Gadget, pro | Reno | 89 |
<docs-resize>
<div class="table-wrap">
<table class="table is-compact is-striped">
<thead>
<tr>
<th>SKU</th>
<th>Product</th>
<th>Warehouse</th>
<th>In stock</th>
</tr>
</thead>
<tbody>
<tr><td>W-1042</td><td>Widget, small</td><td>Columbus</td><td class="as-num">238</td></tr>
<tr><td>W-1043</td><td>Widget, medium</td><td>Columbus</td><td class="as-num">117</td></tr>
<tr><td>W-1044</td><td>Widget, large</td><td>Columbus</td><td class="as-num">42</td></tr>
<tr><td>G-2201</td><td>Gadget, basic</td><td>Reno</td><td class="as-num">1,204</td></tr>
<tr><td>G-2202</td><td>Gadget, pro</td><td>Reno</td><td class="as-num">89</td></tr>
</tbody>
</table>
</div>
</docs-resize>Hoverable
.is-hoverable paints tbody rows with the stripe
tone on hover. Adds an affordance for rows that respond to
click or carry row-level actions.
| Document | Last edited | Shared with |
|---|---|---|
| Q3 roadmap | 14 min ago | Team |
| Hiring plan | Yesterday | Leadership |
| Architecture RFC | 3 days ago | Engineering |
<docs-resize>
<div class="table-wrap">
<table class="table is-hoverable">
<thead>
<tr>
<th>Document</th>
<th>Last edited</th>
<th>Shared with</th>
</tr>
</thead>
<tbody>
<tr><td>Q3 roadmap</td><td>14 min ago</td><td>Team</td></tr>
<tr><td>Hiring plan</td><td>Yesterday</td><td>Leadership</td></tr>
<tr><td>Architecture RFC</td><td>3 days ago</td><td>Engineering</td></tr>
</tbody>
</table>
</div>
</docs-resize>
Variants compose freely.
.is-striped.is-hoverable is a common pair for
long listings; .is-compact.is-bordered works for
dense reference matrices.
Numeric columns
Add .as-num to a <th> or
<td> to right-align the cell and enable
font-variant-numeric: tabular-nums. Right-aligned
numbers let the eye compare magnitudes vertically; tabular
nums keep digit widths consistent so "1,000" and "9,999" line
up column-for-column.
| Invoice | Issued | Subtotal | Tax | Total |
|---|---|---|---|---|
| INV-00412 | 2026-03-01 | $1,240.00 | $99.20 | $1,339.20 |
| INV-00413 | 2026-03-08 | $420.50 | $33.64 | $454.14 |
| INV-00414 | 2026-03-15 | $9,876.00 | $790.08 | $10,666.08 |
<docs-resize>
<div class="table-wrap">
<table class="table is-striped">
<thead>
<tr>
<th>Invoice</th>
<th>Issued</th>
<th class="as-num">Subtotal</th>
<th class="as-num">Tax</th>
<th class="as-num">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>INV-00412</td>
<td>2026-03-01</td>
<td class="as-num">$1,240.00</td>
<td class="as-num">$99.20</td>
<td class="as-num">$1,339.20</td>
</tr>
<tr>
<td>INV-00413</td>
<td>2026-03-08</td>
<td class="as-num">$420.50</td>
<td class="as-num">$33.64</td>
<td class="as-num">$454.14</td>
</tr>
<tr>
<td>INV-00414</td>
<td>2026-03-15</td>
<td class="as-num">$9,876.00</td>
<td class="as-num">$790.08</td>
<td class="as-num">$10,666.08</td>
</tr>
</tbody>
</table>
</div>
</docs-resize>Responsive overflow
When a table has more columns than fit in its container,
.table-wrap clips the overflow and exposes
horizontal scroll.
| Order | Customer | Shipped | Carrier | Tracking | Items | Weight | Subtotal | Shipping | Total |
|---|---|---|---|---|---|---|---|---|---|
| #18421 | Ada Lovelace | 2026-03-12 | UPS | 1Z999AA10123456784 | 3 | 4.2 lb | $120.00 | $12.00 | $132.00 |
| #18422 | Grace Hopper | 2026-03-12 | FedEx | 785436728944 | 1 | 0.8 lb | $38.00 | $7.50 | $45.50 |
| #18423 | Katherine Johnson | 2026-03-13 | USPS | 9400111899223344556677 | 5 | 6.9 lb | $224.00 | $14.00 | $238.00 |
<docs-resize>
<div class="table-wrap">
<table class="table is-striped is-compact">
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th>Shipped</th>
<th>Carrier</th>
<th>Tracking</th>
<th class="as-num">Items</th>
<th class="as-num">Weight</th>
<th class="as-num">Subtotal</th>
<th class="as-num">Shipping</th>
<th class="as-num">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>#18421</td><td>Ada Lovelace</td>
<td>2026-03-12</td><td>UPS</td><td>1Z999AA10123456784</td>
<td class="as-num">3</td><td class="as-num">4.2 lb</td>
<td class="as-num">$120.00</td><td class="as-num">$12.00</td><td class="as-num">$132.00</td>
</tr>
<tr>
<td>#18422</td><td>Grace Hopper</td>
<td>2026-03-12</td><td>FedEx</td><td>785436728944</td>
<td class="as-num">1</td><td class="as-num">0.8 lb</td>
<td class="as-num">$38.00</td><td class="as-num">$7.50</td><td class="as-num">$45.50</td>
</tr>
<tr>
<td>#18423</td><td>Katherine Johnson</td>
<td>2026-03-13</td><td>USPS</td><td>9400111899223344556677</td>
<td class="as-num">5</td><td class="as-num">6.9 lb</td>
<td class="as-num">$224.00</td><td class="as-num">$14.00</td><td class="as-num">$238.00</td>
</tr>
</tbody>
</table>
</div>
</docs-resize>Bordered wrap
The wrap is scroll-only by default -- no outer frame, no
rounded corners. Add .is-bordered to the wrap
for a framed look. When both the wrap and the inner table
carry .is-bordered, the wrap owns the outer
frame and the table's outer border is suppressed so they
don't double up; per-cell borders remain.
| Feature | Status | Ship week |
|---|---|---|
| Command palette | In progress | 14 |
| Dark-mode audit | Review | 15 |
| Webhook retries | Planned | 17 |
<docs-resize>
<div class="table-wrap is-bordered">
<table class="table is-striped">
<thead>
<tr>
<th>Feature</th>
<th>Status</th>
<th class="as-num">Ship week</th>
</tr>
</thead>
<tbody>
<tr><td>Command palette</td><td>In progress</td><td class="as-num">14</td></tr>
<tr><td>Dark-mode audit</td><td>Review</td><td class="as-num">15</td></tr>
<tr><td>Webhook retries</td><td>Planned</td><td class="as-num">17</td></tr>
</tbody>
</table>
</div>
</docs-resize>Bordered table wrap with bordered table
| Feature | Status | Ship week |
|---|---|---|
| Command palette | In progress | 14 |
| Dark-mode audit | Review | 15 |
| Webhook retries | Planned | 17 |
<docs-resize>
<div class="table-wrap is-bordered">
<table class="table is-striped is-bordered">
<thead>
<tr>
<th>Feature</th>
<th>Status</th>
<th class="as-num">Ship week</th>
</tr>
</thead>
<tbody>
<tr><td>Command palette</td><td>In progress</td><td class="as-num">14</td></tr>
<tr><td>Dark-mode audit</td><td>Review</td><td class="as-num">15</td></tr>
<tr><td>Webhook retries</td><td>Planned</td><td class="as-num">17</td></tr>
</tbody>
</table>
</div>
</docs-resize>Class reference
| Class | Role |
|---|---|
.table |
On the <table>. Sets full-width, border-collapse, base padding and typography, bordered thead rule, row separators that drop on the last row. |
.is-striped |
Alternating tbody row background. |
.is-bordered |
Borders on every cell and the outer table. Inside .table-wrap the outer border is suppressed so wrap and table don't double up. |
.is-compact |
Tighter cell padding and smaller font for dense tables. |
.is-hoverable |
Hover background on tbody rows. |
.table-wrap |
On a <div> wrapping the <table>. Horizontal scroll container; no outer frame by default. |
.table-wrap.is-bordered |
Adds an outer border + rounded corners to the wrap. Suppresses .table.is-bordered's outer border when both are applied so the frame doesn't double up. |
.as-num |
On a <th> or <td>. Right-aligns content and enables tabular numbers for column-aligned digits. |
Customization
Table exposes five scoped tokens on .table:
--table-padding (cell padding),
--table-border-color (row separators),
--table-header-bg and
--table-header-color (thead tone), and
--table-stripe-bg (striped rows, also reused by
.is-hoverable). The wrap exposes
--table-wrap-border for the outer frame
(consumed when .is-bordered is applied).
Full token list and defaults:
src/css/table.css.
Accessibility
-
Use proper table semantics. A real
<table>with<thead>,<tbody>, and<th>elements announces correctly to screen readers. Faking a table with<div>rows strips the row/column navigation commands that assistive tech relies on for tabular data. -
Add a
<caption>when the table's purpose isn't obvious from surrounding context. A caption is announced as the table's name and appears visually as a title above it. For tables referenced from prose ("The table below shows..."), the caption is often redundant; for standalone tables it's essential. -
Use
scope="col"andscope="row"on headers. Column headers default to column scope in a simple<thead><tr><th>shape, so explicitscopeis optional there. For complex tables with row headers (first column holds labels), mark those<th scope="row">so screen readers associate each row's cells with the right label. -
Announce sort state with
aria-sort. For tables with sortable columns, addaria-sort="ascending","descending", or"none"on the header cell. Sort triggers go on a button inside the<th>, not on the<th>itself, so the click target is keyboard-accessible. -
Overflow requires keyboard scroll. A
.table-wrapthat scrolls horizontally must be reachable with the keyboard. Addtabindex="0"androle="region"with anaria-labelon the wrap so keyboard users can focus it and arrow-key through the scroll region.
CSS Reference: Table
Styled data table
Source: src/css/table.css
Tokens
| Token | Default | Description |
|---|---|---|
--table-padding | var(--ui-pad-y-tight) var(--ui-pad-x-tight) | Cell padding for th and td |
--table-border-color | var(--color-border) | Border color for cell dividers |
--table-header-bg | transparent | Background color for the header row |
--table-header-color | var(--color-text) | Text color for header cells |
--table-stripe-bg | var(--color-fill-subtle) | Background color for striped and hovered rows |
Classes
| Class | Description |
|---|---|
.table | component root |
.is-striped | Alternating row background on even rows |
.is-bordered | Borders on all cells and the table edge |
.is-compact | Denser padding and smaller font size |
.is-hoverable | Highlight rows on hover |
CSS Reference: Table Wrap
Responsive scroll wrapper
Source: src/css/table.css
Tokens
| Token | Default | Description |
|---|---|---|
--table-wrap-border | var(--border-width-default) solid var(--color-border) | Border for the outer table-wrap when .is-bordered is used |
Classes
| Class | Description |
|---|---|
.table-wrap | component root |
.is-bordered | Show the border for the table wrap |