Skip to content

Tables

Name Email 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>

Markup only; wire the filtering logic yourself. The layout is a flex toolbar above the table.

Order Customer Status
#1001 Ada Lovelace Shipped
#1002 Grace Hopper Processing
#1003 Alan Turing Shipped

Filters

<div class="flex w-full flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<input class="input flex-1" type="search" placeholder="Search orders" />
<button
type="button"
class="btn btn-sm"
commandfor="orders-filters"
command="show-modal"
>
<i class="ti ti-filter" aria-hidden="true"></i> Filters
</button>
<button type="button" class="btn btn-primary btn-sm">
<i class="ti ti-plus" aria-hidden="true"></i> New order
</button>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>#1001</td>
<td>Ada Lovelace</td>
<td>Shipped</td>
</tr>
<tr>
<td>#1002</td>
<td>Grace Hopper</td>
<td>Processing</td>
</tr>
<tr>
<td>#1003</td>
<td>Alan Turing</td>
<td>Shipped</td>
</tr>
</tbody>
</table>
</div>
<dialog id="orders-filters" class="dialog drawer" closedby="any">
<form method="dialog">
<div class="dialog-header">
<h2 class="dialog-title">Filters</h2>
</div>
<div class="dialog-body flex flex-col gap-4">
<div class="field">
<label class="field-label" for="filter-customer">Customer</label>
<input
class="input"
id="filter-customer"
type="search"
placeholder="Any"
/>
</div>
<div class="field">
<label class="field-label">
<input type="checkbox" class="checkbox" /> Unfulfilled only
</label>
</div>
</div>
<div class="dialog-footer">
<button
type="submit"
class="btn btn-ghost"
value="reset"
formnovalidate
>
Reset
</button>
<button type="submit" class="btn btn-primary" value="apply">
Apply
</button>
</div>
</form>
</dialog>
Ada Lovelace 2024-01-12
Alan Turing 2024-03-04
Grace Hopper 2024-05-21
<table class="table">
<thead>
<tr>
<th aria-sort="ascending">
<button type="button" class="btn btn-ghost btn-sm">
Name <i class="ti ti-chevron-up" aria-hidden="true"></i>
</button>
</th>
<th>
<button type="button" class="btn btn-ghost btn-sm">
Created <i class="ti ti-selector" aria-hidden="true"></i>
</button>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ada Lovelace</td>
<td>2024-01-12</td>
</tr>
<tr>
<td>Alan Turing</td>
<td>2024-03-04</td>
</tr>
<tr>
<td>Grace Hopper</td>
<td>2024-05-21</td>
</tr>
</tbody>
</table>

There is no sort component — the click handler and ordering are yours. Set aria-sort to ascending or descending on the sorted column only; leaving it on every column tells a screen reader they are all sorted.

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>
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>
SKU Name Qty
A-001 Widget 128
A-002 Gadget 64
A-003 Sprocket 32
<table class="table table-compact table-striped">
<thead>
<tr>
<th>SKU</th>
<th>Name</th>
<th data-align="right">Qty</th>
</tr>
</thead>
<tbody>
<tr>
<td>A-001</td>
<td>Widget</td>
<td data-align="right">128</td>
</tr>
<tr>
<td>A-002</td>
<td>Gadget</td>
<td data-align="right">64</td>
</tr>
<tr>
<td>A-003</td>
<td>Sprocket</td>
<td data-align="right">32</td>
</tr>
</tbody>
</table>
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>
Name Email Team Role Joined
Ada Lovelace ada@example.com Platform Admin 2021-04-02
Grace Hopper grace@example.com Compilers Editor 2020-11-18
<div style="overflow-x: auto; max-width: 360px">
<table class="table table-pin-col">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Team</th>
<th>Role</th>
<th>Joined</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ada Lovelace</td>
<td>ada@example.com</td>
<td>Platform</td>
<td>Admin</td>
<td>2021-04-02</td>
</tr>
<tr>
<td>Grace Hopper</td>
<td>grace@example.com</td>
<td>Compilers</td>
<td>Editor</td>
<td>2020-11-18</td>
</tr>
</tbody>
</table>
</div>
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>
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>

Put a Checkbox in the first cell. Wire the select-all header checkbox yourself.

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>
#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>
Order Customer Total
<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>

The row-filling hit area is an ::after on the row’s first <a>. Any other link or button in the row must establish its own stacking context — position: relative plus a non-auto z-index — or the pseudo-element swallows its clicks. .btn already qualifies.

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>Total</td>
<td data-align="right">$201.50</td>
</tr>
</tfoot>
</table>
Item Amount
Widget $129.00
Gadget $72.50
Subtotal $201.50
Tax $20.15
Total +12% $221.65
<table class="table">
<thead>
<tr>
<th>Item</th>
<th data-align="right">Amount</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>Subtotal</td>
<td data-align="right">$201.50</td>
</tr>
<tr>
<td>Tax</td>
<td data-align="right">$20.15</td>
</tr>
<tr>
<td>
Total
<span class="badge badge-success badge-soft">
<i class="ti ti-trending-up" aria-hidden="true"></i>
+12%
</span>
</td>
<td data-align="right">$221.65</td>
</tr>
</tfoot>
</table>
Order Customer Total
No orders match your filters.
<table class="table">
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th data-align="right">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-empty" colspan="3">No orders match your filters.</td>
</tr>
</tbody>
</table>

A table never owns a scroll region. sticky, pinCol, and horizontal overflow on narrow viewports all need you to supply the scrolling ancestor:

<div style="overflow: auto; max-height: 240px">
<table class="table table-sticky">
</table>
</div>
Part Renders Class
Table <table> table
Table.Head <thead>
Table.Body <tbody>
Table.Foot <tfoot>
Table.Row <tr> table-row-link when asLink
Table.HeaderCell <th scope="col"> table-header-cell
Table.Cell <td> table-cell
Table.Empty its own <tr> + <td> table-empty
Part Prop Type Default
Table striped boolean false
Table bordered boolean false
Table density "compact" | "default" | "relaxed" "default"
Table relaxed boolean false
Table sticky boolean false
Table pinCol boolean false
Table.Row selected boolean false
Table.Row asLink boolean false
Table.Cell, Table.HeaderCell align "left" | "right" | "center" "left"
Table.Cell, Table.HeaderCell gutter boolean false
Table.Cell numeric boolean false

relaxed is deprecated — use density="relaxed". sticky needs a scrolling ancestor and pinCol an overflow-x one. selected sets [data-selected] for checkbox-less selection; asLink styles the row but you still supply the <a>. align="left" and density="default" emit nothing. Set gutter on the header cell too so the column lines up, and Table.Empty takes a colSpan — set it to the column count.

Plus each element’s native attributes. Table takes no classNames — every part accepts className. Table.Empty renders its own <tr>, so drop it straight into Table.Body.

Class / var Effect
table Full width, text-sm, collapsed borders; cells get 0.75rem/0.375rem padding and a bottom divider (~32px rows)
table-striped Tints even <tbody> rows
table-bordered Border on the table and every cell
table-compact 0.5rem/0.25rem padding, text-xs
table-relaxed 1rem/0.75rem padding
table-sticky Pins <thead> cells to the top of the scroll region
table-pin-col Pins the first cell of every row against horizontal scroll
table-header-cell Header styling for a cell outside <thead>
table-cell Cell styling for markup that isn’t a real <td>
table-cell-numeric Right-aligns and uses tabular figures
table-cell-gutter 1.5rem centered status column with muted text — colour the icon yourself when status carries meaning
table-empty Centered muted message cell; set colspan to the column count
table-row-link Row-filling hit area taken from the row’s first <a>

Modifiers compose — striped with sticky with relaxed is fine.

Plain <th> and <td> need no class: descendant selectors style them, and <tfoot> rows are semibold with a strong top divider on the first automatically. [data-align="right"] or [data-align="center"] on a cell aligns it; left is the default. The row tint responds to input[type="checkbox"]:checked, .checkbox[data-checked], or [data-selected] on the <tr>.

Striping, hover and selection are scoped to <tbody>, and table-pin-col pins :first-child — so the pinned column must literally be first, and a loose <tr> outside <tbody> won’t stripe.