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.

NameRoleStatus
Ada LovelaceEngineerActive
Grace HopperEngineerActive
Show 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 (th and td) is start-aligned by default — including <th>, overriding the browser’s default centering — via a @layer components rule, so a text-end / text-center utility 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.

NameRoleOffice
Ana RuizDesignLisbon
Tomás OrtizEngineeringPorto
Column 1Column 2Column 3Column 4Column 5Column 6Column 7Column 8Column 9Column 10Column 11Column 12
Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12
Show 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.

NameStatusProjects
ALAna LimaActive12
ZPZoe ParkInvited0
MRMarco RicciActive7
LNLea NovakPaused3
Show 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}:

smallRole
Ana RuizDesign
Tomás OrtizEngineering
Li WeiSupport
mediumRole
Ana RuizDesign
Tomás OrtizEngineering
Li WeiSupport
stripedRole
Ana RuizDesign
Tomás OrtizEngineering
Li WeiSupport
plainRole
Ana RuizDesign
Tomás OrtizEngineering
Li WeiSupport
Show 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:

NameRole
Ada LovelaceDeveloper
Grace HopperEditor
Show code
<Table headerStyle="caps">…</Table>

End-align a numeric column:

ItemPrice
Widget$12.00
Gadget$34.50
Show 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.

NameRole
Ana RuizDesign
Tomás OrtizEngineering
Show 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> a scope="col" (column header) or scope="row" (row header) — Table styles headers but doesn’t add scope for you.
  • Add a <caption> (visually hidden with sr-only if 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 a role="region" with an aria-label on 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 add scope for you.
  • Do end-align numeric columns (text-end on 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 title attribute 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.