Data table
Data tables should support scanning, sorting, filtering, and repeated comparison. Keep column labels stable, expose sort direction, and avoid hiding essential data behind decorative layouts.
| Anchor index | 15 |
| Breadcrumbs | 10 |
| Button | 20 |
When to use#
- A table supports scanning, sorting, filtering, and repeated comparison; keep column labels stable, expose the sort direction, and avoid hiding essential data behind decorative layouts
- Use the shared table wrapper whenever a table needs interactive sorting
- When a table-like page exposes active filters, use the shared filter row partial rather than metadata tag classes — filter chips change visible rows; metadata tags only describe content
- Keep column widths consistent across repeated tables — prefer explicit width utilities on matching header and body cells, and use
fixed: truewhen several tables on the same page should align visually
Implementation#
{{ partial "data-page/table-wrap.html" (dict "minWidth" "56rem" "sortable" true "fixed" true) }}
<thead>…</thead>
<tbody>…</tbody>
{{ partial "data-page/table-wrap.html" (dict "mode" "close") }}<div class="overflow-x-auto border-y border-line" style="--data-table-min-width: 56rem;" tabindex="0" role="region" aria-label="Scrollable table">
<table class="w-full min-w-[var(--data-table-min-width)] table-fixed border-collapse text-left text-sm" data-sortable-table>
<thead>
<tr class="border-b border-line">
<th scope="col" aria-sort="ascending">
<button class="table-sort-button" type="button" data-table-sort="name">Name</button>
</th>
</tr>
</thead>
<tbody>
<tr data-table-row>
<td data-sort-value="2026-08-11">2026.08.11</td>
</tr>
</tbody>
</table>
</div>Sorting is wired by initTableLists in assets/js/site.js. Its contract:
- Wrap the table area in an element with
data-table-list; the JavaScript looks for one sortable table (data-sortable-table) inside that wrapper - Column headers use
table-sort-buttonand a stabledata-table-sortkey; the initially sorted<th>must setaria-sorttoascendingordescending, and inactive column headers omit it or use the value managed by JavaScript - Rows must use
data-table-row; each sortable cell should providedata-sort-value, especially when the visible text includes links, badges, dates, counts, or localized labels
The Hugo partial is layouts/partials/data-page/table-wrap.html, called once to open the scroll wrapper and <table> and once with mode: "close" to close them. The wrapper itself carries tabindex="0", role="region", and the dataTableScrollRegion i18n label, so keyboard users can reach and scroll it independent of the table’s own content; <th> scope is set by the calling page, not the partial, since only the page authoring the row knows whether a header names a column (scope="col", the common case) or a row (scope="row", used where the first cell in a data row identifies that row, as on the Starfleet registry table). Sorting behaviour lives in assets/js/site.js (initTableLists); sort affordances and direction symbols live in assets/css/site.css. The talks archive predates this contract and runs a parallel behaviour (initTalksArchive with data-talks-* attributes) on the same visual classes.
Options#
| Option | Purpose |
|---|---|
minWidth | Minimum table width before the wrapper scrolls horizontally (default 48rem) |
sortable | Adds data-sortable-table so the shared sorting behaviour can attach |
fixed | Adds table-fixed so repeated tables on one page align visually |
caption | Screen-reader-only <caption> for the table |
class | Extra classes on the scroll wrapper |
tableClass | Extra classes on the <table> |
tableAttrs | Extra attributes on the <table> |
mode | open (default) or close — the partial is called once on each side of the body |
Interface manifest
- Kind
- Component
- Category
- Data
- Status
- Implemented
- Implementation
- Partial:
layouts/partials/data-page/table-wrap.htmlLayout:layouts/_default/_markup/render-table.htmlCSS:assets/css/site.cssJavaScript:assets/js/site.jsi18n:dataTableScrollRegion
Accessibility#
- The component is a real
<table>with<thead>and<tbody>inside a scrolling<div>; passcaptionso screen readers get a name for the table — the partial renders it as a visually hidden<caption> - Every
<th>carriesscope="col"orscope="row", set by the calling page, so screen readers announce which column or row a cell belongs to instead of relying on the implicit position of<th>in<thead> - The scroll wrapper carries
tabindex="0",role="region", and thedataTableScrollRegioni18n label, so keyboard-only users can reach and scroll wide tables even when no cell inside is focusable - Sort headers are native
<button type="button">elements inside<th>cells: Tab reaches each one and Enter or Space re-sorts, with no custom key handling initTableListskeepsaria-sortcurrent —ascending,descending, ornone— on each<th>column header, where ARIA defines the attribute; it does not putaria-sorton the nested button- Avoid using a table for layout —
<table>announces row and column structure to screen readers; decorative grids belong in CSS