Table
A styled, horizontally-scrollable table. Write plain thead/tbody markup — Table only styles it.
Last updated
Uses JavaScript
Fades the hidden edge while the table scrolls sideways and makes the scrolling wrapper a tab stop only while it overflows. With sortable or selectable the same island adds sort buttons and wires the row checkboxes.
Without JS: The table still scrolls sideways and shows its rows in source order; sort buttons and select-all do not appear.
| Name | Role | Status |
|---|---|---|
| Ada Lovelace | Engineer | Active |
| Grace Hopper | Engineer | Active |
Show codeHide code
<Table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ada Lovelace</td>
<td>Engineer</td>
<td>Active</td>
</tr>
<tr>
<td>Grace Hopper</td>
<td>Engineer</td>
<td>Active</td>
</tr>
</tbody>
</Table>Add
npx @ava-ui/apogee add table
Depends on utils (fade.ts, table-enhance.ts). Uses @lucide/astro for the sort chevrons.
Usage
---
import Table from '@/components/apogee/Table/Table.astro'
---
<Table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ada Lovelace</td>
<td>Engineer</td>
</tr>
</tbody>
</Table>
Anatomy
Table has no sub-components or named slots — it styles the real table
elements you write, via descendant selectors scoped to an internal
.apogee-table class:
- Default slot — write a plain
<thead>/<tbody>(and optional<tfoot>) with<tr>/<th>/<td>inside, exactly as you would in raw HTML. <th>— gets padding and semibold weight; a<thead>’s<th>additionally gets the bottom border rule that separates it from the body.<td>— gets padding and a top border rule, which reads as the row separator.- Every cell (
thandtd) is start-aligned by default — including<th>, overriding the browser’s default centering — via a@layer componentsrule, so atext-end/text-centerutility on any individual cell still wins. - The whole table is wrapped in an
overflow-x-auto<div>automatically, so wide tables scroll horizontally instead of breaking the page layout — no prop needed.
API
@composable — exposed to Content Editors in AVA. Any other attribute
passes through to the <table> element (not the scroll wrapper <div>).
| Prop | Type | Default | Description |
|---|---|---|---|
headerStyle |
'normal' | 'caps' |
'normal' |
Header text: sentence case and semibold, or small caps. |
border |
boolean |
false |
Outer border with rounded corners. |
columnBorder |
boolean |
false |
Hairline between columns. |
size |
'small' | 'medium' |
'small' |
Cell padding. |
striped |
boolean |
false |
Alternate row fills. |
headerFill |
boolean |
true |
A faded fill behind the header row, matching DataGrid. |
sortable |
boolean |
false |
Every header with text becomes a sort button. A cell’s data-value is its sort key (use it when the cell holds an avatar or badge); data-sortable="false" on a header skips it, and a header holding a control (a checkbox, a link) is never wrapped. |
selectable |
boolean |
false |
Wires <Checkbox data-row> cells and one <Checkbox data-all> (indeterminate when some rows are checked) and tints checked rows. |
class |
string |
— | Extra classes on the <table>, merged last. |
Borders and overflow
The wrapper scrolls sideways and fades the hidden edge.
| Name | Role | Office |
|---|---|---|
| Ana Ruiz | Design | Lisbon |
| Tomás Ortiz | Engineering | Porto |
| Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 | Column 7 | Column 8 | Column 9 | Column 10 | Column 11 | Column 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
Show codeHide code
<Table border columnBorder>…</Table>
<div class="max-w-md"><Table>…12 columns…</Table></div>Sorting and selection
Rich cells and sorting in one table. The name cell holds an avatar, so it
carries data-value as its sort key; the checkbox header has no text and is
skipped.
| Name | Status | Projects | |
|---|---|---|---|
| ALAna Lima | Active | 12 | |
| ZPZoe Park | Invited | 0 | |
| MRMarco Ricci | Active | 7 | |
| LNLea Novak | Paused | 3 |
Show codeHide code
<Table sortable selectable>
<thead><tr>
<th><Checkbox data-all aria-label="Select all" /></th>
<th>Name</th><th>Status</th><th class="text-end">Projects</th>
</tr></thead>
<tbody>
<tr>
<td><Checkbox data-row aria-label="Select Ana Lima" /></td>
<td data-value="Ana Lima"><Avatar name="Ana Lima" size="xsmall" /> Ana Lima</td>
<td><Badge size="small" color="positive">Active</Badge></td>
<td class="text-end">12</td>
</tr>
…
</tbody>
</Table>Density and stripes
Both size values, striped, and headerFill={false}:
| small | Role |
|---|---|
| Ana Ruiz | Design |
| Tomás Ortiz | Engineering |
| Li Wei | Support |
| medium | Role |
|---|---|
| Ana Ruiz | Design |
| Tomás Ortiz | Engineering |
| Li Wei | Support |
| striped | Role |
|---|---|
| Ana Ruiz | Design |
| Tomás Ortiz | Engineering |
| Li Wei | Support |
| plain | Role |
|---|---|
| Ana Ruiz | Design |
| Tomás Ortiz | Engineering |
| Li Wei | Support |
Show codeHide code
<Table size="small">…</Table>
<Table size="medium">…</Table>
<Table striped>…</Table>
<Table headerFill={false}>…</Table>States
Hover a row in the live preview above — tbody tr:hover gets a
bg-neutral-faded fill.
Examples
Small-caps headers:
| Name | Role |
|---|---|
| Ada Lovelace | Developer |
| Grace Hopper | Editor |
Show codeHide code
<Table headerStyle="caps">…</Table>End-align a numeric column:
| Item | Price |
|---|---|
| Widget | $12.00 |
| Gadget | $34.50 |
Show codeHide code
<Table>
<thead>
<tr>
<th>Item</th>
<th class="text-end">Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget</td>
<td class="text-end">$12.00</td>
</tr>
<tr>
<td>Gadget</td>
<td class="text-end">$34.50</td>
</tr>
</tbody>
</Table>A table card: Card regions hold the title row and the pagination footer.
Members
Everyone with access to this workspace.
| Name | Role |
|---|---|
| Ana Ruiz | Design |
| Tomás Ortiz | Engineering |
Show codeHide code
<Card padding="none">
<SectionHeader slot="header" size="small" heading="Members" description="Everyone with access." class="flex-1" />
<Button slot="header" size="small">Invite</Button>
<Table>…</Table>
<Pagination slot="footer" currentPage={1} totalPages={8} class="ms-auto" />
</Card>A captioned table (caption hidden visually, still announced):
<Table>
<caption class="sr-only">Team members</caption>
<thead>
<tr><th scope="col">Name</th><th scope="col">Role</th></tr>
</thead>
<tbody>
<tr><td>Ada Lovelace</td><td>Engineer</td></tr>
</tbody>
</Table>
Accessibility
- Real
<table>/<thead>/<tbody>/<tr>/<th>/<td>— full table semantics for free; Table only adds styling. - Give every
<th>ascope="col"(column header) orscope="row"(row header) — Table styles headers but doesn’t addscopefor you. - Add a
<caption>(visually hidden withsr-onlyif you don’t want it shown) to name the table for screen readers. - For a wide table relying on the horizontal-scroll wrapper, consider adding
tabindex="0"and arole="region"with anaria-labelon the scroll container if it needs to be reachable and announced as its own scrollable region.
Tokens
| Token | Used for |
|---|---|
--foreground-neutral |
Header + cell text |
--border-neutral |
Header bottom rule + row top rule |
--background-neutral-faded |
Row hover, header fill, stripes, checked rows |
Do / Don’t
- Do write real
<th scope="col">headers — Table styles them but doesn’t addscopefor you. - Do end-align numeric columns (
text-endon the<td>/<th>) — the default is start-aligned. - Don’t put interactive controls (buttons, inputs) in cells without their own visible focus state — Table doesn’t style them.
- Don’t rely on horizontal scroll alone for a critical column — put the most important column first (reading order follows source order, and mirrors under RTL).
Content
- Header labels are short nouns, sentence case, no ending punctuation.
- Truncate long cell text with an explicit
titleattribute holding the full value, not silent CSS overflow with no fallback. - Show a neutral placeholder (“—”) for an empty cell rather than leaving it blank, so a value isn’t mistaken for missing data.