Table
Data table for displaying structured information with sortable columns, row selection, resizable columns, and sticky headers.
| Destination | Departure | Duration | Price |
|---|---|---|---|
| Mallorca | 15 Aug 2025 | 7 nights | €499 |
| Tenerife | 22 Aug 2025 | 14 nights | €799 |
| Lanzarote | 01 Sep 2025 | 10 nights | €649 |
Anatomy
| Column A | Column B | Column C |
|---|---|---|
| Cell | Cell | Cell |
| Cell | Cell | Cell |
- 1 Header row — sticky column labels, optionally sortable or resizable
- 2 Body row — data cells, optionally selectable
- 3 Table wrapper — horizontal scroll container
Behaviors
| Destination | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
Sortable columns
Clicking or keyboard-activating a th[aria-sort] header cycles through none → ascending → descending → none. Only one column is sorted at a time; activating a new header resets the previous sort.
| Destination | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
Row selection
data-selection="single" allows one row at a time; multi allows multiple rows. The selected state is tracked via aria-selected="true" on the <tr>. Multi-selection supports a select-all checkbox in the header.
| Destination | Departure | Price |
|---|---|---|
| Mallorca | 15 Aug 2025 | €499 |
Resizable columns
Adding data-resizable to a <th> injects a drag handle. Dragging resizes the column inline; a minimum width of 96 px is enforced. Column widths are not persisted by default.
| Destination | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
| Lanzarote | €649 |
| Rhodes | €589 |
| Gran Canaria | €529 |
| Ibiza | €459 |
| Fuerteventura | €699 |
Sticky header
The <thead> row sticks to the top of the scroll container via position: sticky so column labels remain visible as the user scrolls through long tables.
Options
Density
| Name | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
| Lanzarote | €649 |
| Rhodes | €589 |
| Name | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
| Lanzarote | €649 |
| Rhodes | €589 |
| Name | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
| Lanzarote | €649 |
| Rhodes | €589 |
Options reference
| Attribute | Values | Default |
|---|---|---|
density | compact / comfortable / spacious | — |
data-selection | single / multi | — |
aria-sort on th | none / ascending / descending | — |
data-resizable on th | boolean attribute | — |
Keyboard interactions
| Key | Context | Action |
|---|---|---|
| Enter / Space | Sortable header | Cycles sort direction: none → ascending → descending → none |
| Enter / Space | Selectable row | Toggles row selection |
| Tab | Table | Moves focus through interactive cells and controls |
Usage guidelines
Use selection only when the action is clear
| Destination | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
Do
Use selection when the user needs to act on a row — e.g. delete, compare, or bulk-update.
| Destination | Price |
|---|---|
| Mallorca | €499 |
| Tenerife | €799 |
Don't
Don't add row selection to display-only tables — it implies an action that doesn't exist.
Match density to context
| Name | Status | Value |
|---|---|---|
| Item A | Active | 100 |
| Item B | Draft | 200 |
| Item C | Active | 150 |
Do
Use compact density for data-heavy dashboards where many rows must be visible at once.
| Name | Status | Value |
|---|---|---|
| Item A | Active | 100 |
| Item B | Draft | 200 |
Don't
Don't use spacious density in compact UIs — it wastes vertical space and forces unnecessary scrolling.
Accessibility
- Use a
<caption>element oraria-labelon the<table>to give it an accessible name. - Sortable
<th>elements usearia-sort(none/ascending/descending) to communicate sort state to screen readers. - Selected rows use
aria-selected="true"on<tr>; the table must haverole="grid"or the selection pattern must be documented for AT users. - Column resize handles are visually-only; ensure column widths can also be controlled by other means.
- Focus ring uses
box-shadowinset to avoid layout impact and remains visible in high-contrast modes. - Do not rely on color alone to communicate sort direction — the
↑/↓/↕icons provide a redundant visual cue.
Content standards
Column headers
Keep column headers short — 1–3 words. Use sentence case, not title case. Avoid abbreviations unless they are universally understood (e.g. "ID", "SKU").
| Destination | Departure date | Price |
|---|---|---|
| Mallorca | 15 Aug 2025 | €499 |
Do
Use clear, concise labels that describe the column's data.
| Dest. | Dep. Dt. | Pr. (€) |
|---|---|---|
| Mallorca | 15/08/25 | 499 |
Don't
Don't use unclear abbreviations that force the user to guess what the column means.
Empty and loading states
Provide a meaningful message for empty tables. Use <td colspan="…"> with the .uif-table-empty class. For loading state, add .uif-table--loading to the <table> element.
Theming
Table adapts automatically across brands and color modes through component tokens (--uif-table-*). Background, text, border, hover, and selection colors all respond to brand and mode changes. Use the hero preview switches above to see it in action.
For the full theming architecture — brands, modes, and how tokens cascade — see Foundations: Design Tokens.
Design checklist
--uif-table-*) defined.schemas/web-table.figma.ts has a placeholder.Usage
HTML
<div class="uif-table-wrapper">
<table class="uif-table" data-selection="single">
<thead>
<tr>
<th aria-sort="none">Destination</th>
<th aria-sort="none">Departure</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr><td>Mallorca</td><td>15 Aug 2025</td><td>€499</td></tr>
</tbody>
</table>
</div>
<script type="module" src="/vendor/ui-foundations/components/table.js"></script>
JavaScript
import { enhanceTable } from "ui-foundations/ui/components/table.js";
// Enhance all tables on the page:
enhanceTable();
// Or enhance within a specific container:
enhanceTable(document.querySelector("#my-container"));