Table
Displays structured data in rows and columns with support for sorting, pagination, filtering, and row selection. Use for dense data sets that require comparison or scanning. Don't use for simple key-value data.
Description
What it does
Presents tabular data in a structured grid of rows and columns, driven by JavaScript data and column definitions rather than hand-written markup. Supports sorting, global search, per-column filters, row selection, pagination, column pinning/reordering/resizing, grouping, and CSV export — all opt-in per grid.
Where it appears
Stakeholder lists, consultation response summaries, submission logs, audit trails, and any screen that requires scanning or comparing multiple records side by side.
Why it exists
Government workflows frequently involve reviewing large sets of structured records — stakeholder registers, public submissions, engagement histories. The table component provides a consistent, accessible, keyboard-navigable pattern for these dense data contexts without requiring teams to hand-build a grid or reimplement ARIA table semantics themselves.
Anatomy
| Part | Required? | Notes |
|---|---|---|
| Grid element | Required | The wa-data-grid element itself. Takes a label (accessible name), a row-key (the field uniquely identifying each row), and .data / .columns set as JavaScript properties — not HTML attributes, since both are arrays/objects. |
| Column definition | Required | Each entry in .columns maps a field to a header label, with optional sortable, width/flex, align, and a formatter function for custom cell content (badges, buttons, computed values). |
| Header row | Required | Rendered from column definitions. Shows sort indicators, optional per-column filter controls, and a per-column menu (pin/sort/hide/autosize) when with-column-menu is set. |
| Selection checkbox column | Optional | Added automatically when selectable is set. Includes a header select-all checkbox with indeterminate state handled internally. |
| Actions column | Optional | A column with no field, just an id and a formatter rendering buttons/links/menus for row-level actions. Give each icon-only action button its own accessible name. |
| Empty / loading / no-results slots | Optional | Named slots (empty, loading, no-results) for custom content when there's no data, a server request is in flight, or an active search/filter matches nothing. |
| Pager | Optional | Rendered automatically in the footer when paginate is set — a wa-pagination element under the hood, stylable via the pager CSS part. |
Variants
Outlined (default)
Bordered grid, the default appearance.
Use for nearly every table — a standalone list of records on its own page or panel.
Sortable
One or more columns can reorder the data set ascending or descending.
Set sortable: true on any column whose values have a meaningful order — date, name, count, status.
Selectable
Each row has a checkbox allowing individual or bulk selection, enabling bulk actions on the selected set.
Set selectable (bare attribute = multiple) when users need to perform batch operations — bulk export, bulk status change, bulk assignment. Use selectable="single" for radio-style, one-row-at-a-time selection.
Paginated
Rows are split into pages with a pager control in the footer.
Set paginate and page-size once a data set is large enough that scrolling alone becomes the primary way users would otherwise navigate it.
Empty
No rows match the current data set, filters, or search.
The empty slot renders automatically whenever data is an empty array — always give it real explanatory content and, where relevant, a call to action.
Plain
Borderless grid that blends into the surrounding surface.
Use when the grid is already inside a bordered container (e.g. a card or panel) and a second border would be redundant.
Striped
Alternating row backgrounds improve scanability for wide or dense tables.
Set striped when column count is high (six or more) or cell content is long, making it difficult to track a row across the full width.
Size scale
Controls text scale, row height, and cell padding together (xs–xl).
Use the default (m) in most contexts. Drop to s or xs for dense operational views where scanning many rows matters more than touch-target comfort; reserve that trade-off for non-touch, desktop-only surfaces.
States
| State | Behaviour |
|---|---|
| Default | Rows render with the standard grid surface and row divider. Sortable columns show an inactive sort affordance in the header. |
| Row hover | Hovered row background shifts to indicate focus of attention. Does not affect sort or selection state. |
| Row selected | Selected rows are visually distinguished and their checkbox is checked. selectedKeys / selectedRows are the source of truth; read them or listen for wa-row-select. |
| Column sorted (ascending / descending) | The active column's header shows a directional sort indicator. Third click on a sorted column clears the sort by default (set without-sort-removal to keep it always sorted, alternating direction instead). |
| Loading | In server mode, set the loading property (or let the grid manage it automatically while dataSource resolves). Slot custom content into the loading slot for a bespoke overlay. |
| Empty | When data is an empty array, the empty slot's content renders in place of rows. Provide an explanation and, where relevant, a call to action (e.g. "Create engagement"). |
| No results | When an active search or column filter matches nothing, the no-results slot renders instead of the empty slot — falls back to a localized default message if not provided. |
| Row locked / non-selectable | Set selectableRows to a predicate returning false for rows that shouldn't be selectable — their checkbox is disabled and they're skipped by select-all and range selection. |
Usage guidelines
When to use
- Displaying a list of stakeholders with attributes such as name, organisation, status, and last contacted date.
- Showing public consultation submissions with columns for submitter, submission date, topic, and review status.
- Presenting an engagement activity log where users need to sort by date or filter by type.
- Bulk-managing records, such as reassigning a group of stakeholders to a new project officer.
- Comparing structured data across multiple records where alignment by column is meaningful.
When not to use
- For simple key-value metadata about a single record — use a Description List instead.
- For a small set of cards or tiles where visual hierarchy matters more than column alignment — use a Card List.
- For calendar or timeline data where temporal position is the primary axis — use a Timeline or Calendar component.
- For a single flat list where there is only one meaningful attribute — use a List component.
Do / Don't
Do
Always set the label attribute so assistive technology has an accessible name for the grid.
Don't
Don't rely on surrounding heading text alone to label the grid — a heading isn't programmatically associated with it.
Do
Set row-key to a stable, unique field (e.g. a database id) whenever selectable or pagination is used, so selection survives sorting and page changes.
Don't
Don't use an array index or a field that can repeat as row-key — selection and expansion state will behave unpredictably.
Do
Keep column count to a maximum of seven or eight on desktop. Prioritise the most important attributes and use a row detail panel or a separate page for secondary data.
Don't
Don't add more columns to avoid building a detail page — overly wide grids force horizontal scrolling and overwhelm users.
Do
Show meaningful empty and no-results slot content, explaining what happened and how to adjust.
Don't
Don't leave the empty/no-results slots unset when the default message won't make sense for your data — users can't tell whether data is missing, filtered out, or still loading.
Do
Place bulk action controls (e.g. Export selected, Change status) directly above or below the grid, enabled only when rows are selected.
Don't
Don't trigger destructive bulk actions immediately on selection — always require an explicit confirm step.
Do
Give every icon-only action-column button its own accessible name that includes the row subject, e.g. "Edit Draft Transport Strategy".
Don't
Don't use a bare "Edit" or "Delete" label on row actions — a screen reader user scanning many rows can't tell which record each button acts on.
Layout & Spacing
Row height, cell padding, and text scale all move together via the size attribute (xs, s, m, l, xl — default m). Column widths: set width for a fixed pixel width, or flex/minWidth to share remaining space proportionally. Unset columns share space equally. Grid border radius and outer spacing come from wa-data-grid's own --wa-* panel tokens (see CSS Custom Properties in Web Awesome's docs) — Orbit hasn't layered semantic --op-* overrides onto this component yet.
Tokens
| Part | Token | Value |
|---|---|---|
| Row/body surface (outlined) | --wa-color-surface-default → scoped to --wa-color-surface-raised | Same underlying token family as wa-card's outlined appearance, and scoped to raised the same way — via component-overrides.css, not the global token every other surface-default component also reads. The header uses its own --header-background (already --wa-color-surface-lowered by default), so it stays visually distinct from the raised rows. |
| Header text | --wa-color-text-quiet | Slightly subdued relative to data cell text to create visual hierarchy, consistent with Orbit's header-vs-body text convention elsewhere. |
| Data cell text | --wa-color-text-normal | Full-weight text for scannable data content. |
| Selected/active row accent | --wa-color-brand-fill-loud (or equivalent) | Verify against the current token set once a real Open Point or Social Point screen adopts this component — no Orbit-specific override exists yet, unlike wa-card's surface scoping. |
| Focus ring | --op-color-interactive-focus | Should match every other interactive element in Orbit. Confirm the grid's internal focus-ring token maps to this rather than a generic browser default. |
Engineering notes
- Use the wa-data-grid element. Set .data and .columns as JavaScript properties (not HTML attributes) — they're arrays/objects, not strings.
- Always set row-key to a stable unique field. Without it, selection and expansion state can't be tracked reliably across sorts, filters, and pages.
- A column needs a field to participate in sorting, filtering, search, copy, and CSV export. An actions column has no field — just an id and a formatter — and is correctly excluded from all of those.
- formatter receives (value, row) and can return a plain string (escaped automatically) or a lit html template for rich content (badges, buttons, links). Never build cell HTML via string concatenation — use the lit html tag so interpolated values stay escaped.
- Client-side sorting/filtering/pagination is automatic. For large or server-backed data sets, set dataSource (an async function) or server + listen for wa-data-request — the grid switches to manual mode and you control what data.length /.total the pager reflects.
- Don't reimplement sort/selection/pagination with a hand-rolled
— this component exists specifically so teams stop doing that per-screen, each with slightly different accessibility gaps.
Keyboard interaction
Key Action Tab / Shift+Tab Moves focus into or out of the grid as a single stop (not per-cell) — the grid manages internal focus with a roving tabindex, per the ARIA grid pattern. Arrow keys Once the grid has focus, move between cells (including into the header row). Home / End Move to the first / last cell in the current row. Ctrl+Home / Ctrl+End Move to the first / last cell in the entire grid. Page Up / Page Down Move by a page of rows. Enter Sorts the focused header column, toggles a row's expansion from its expand cell, or activates a data cell (emits wa-cell-click). Space Toggles selection of the focused row. Shift+Up / Shift+Down Extends the selection. Ctrl+A Selects all rows (the current page's rows, when paginated). Ctrl+C Copies the selected rows to the clipboard. Shift+Left / Shift+Right Reorders the focused header column (when reorderable). Alt+Left / Alt+Right Resizes the focused header column (when resizable). Why it matters
Government datasets often include sensitive information about individuals — stakeholders, submitters, community members. A grid that lacks proper roving-focus and ARIA grid semantics forces assistive technology users into a much slower, error-prone workflow to review or act on records, which is a genuine compliance risk under the Disability Discrimination Act and equivalent legislation elsewhere. Building this once, correctly, in the underlying component is what makes every screen that uses it accessible by default rather than by individual implementation effort.
Focus
The grid is a single tab stop; once focused, arrow keys move a roving internal focus between cells (never re-entering Tab order per cell). Sorting, filtering, selecting, and paging never move focus outside the grid unexpectedly — focus stays on the control that was activated (a header cell, a checkbox, a pager button). Row selection state (selectedKeys / selectedRows) and view state (sort, filters, column order/widths/visibility) can be captured with getState() and restored with setState() — useful for preserving a user's view across navigation.
ARIA
Role or attribute When to use Example role="grid" (or "treegrid")Applied to the component root automatically. treegrid is used when rows have children (tree data via childRows). <wa-data-grid label="Stakeholder register" row-key="id"></wa-data-grid>labelThe grid's accessible name. Always set it — there is no visible caption element to fall back on. <wa-data-grid label="Consultation submissions" row-key="id"></wa-data-grid>aria-label (action buttons)Every icon-only button in an actions-column formatter needs its own accessible name including the row subject. <wa-button size="s" appearance="plain"><wa-icon name="pencil" label="Edit Draft Transport Strategy"></wa-icon></wa-button>Live region announcementsThe grid announces selection counts, filter/search results, clipboard copies, and column moves through its own polite live region — no additional markup required. (handled internally by the component)Contrast
Header text against the grid surface, and data cell text against the row background, both need to meet 4.5:1 — verify once Orbit's own token overrides are applied to this component (none exist yet, see the Style tab). Any status badge rendered inside a formatter follows Badge's own contrast rules. Focus indicators on cells and header controls must meet the same 3:1 non-text contrast requirement as every other interactive element in Orbit.
Touch targets
Selection checkboxes, per-column menu buttons, sort headers, and action-column buttons all need to meet the 44x44px minimum touch target on any surface that supports touch. The compact (xs/s) size variants trade touch comfort for density — reserve them for desktop-only, mouse/keyboard operational views.
Things to avoid
- Do not hand-roll a semantic <table> alongside wa-badge/wa-button cell content to replicate what this component already does — that was Orbit's own pre-3.11 stopgap and is exactly the gap this component closes.
- Do not rely on colour alone to communicate sort direction or row selection — the grid pairs colour with an icon/indicator by default; don't strip that in custom styling.
- Do not give an actions-column formatter's buttons generic labels — see callout_watch.
- Do not use row-key values that can collide or change across a session (e.g. an array index) — selection, expansion, and sort stability all depend on it being a genuinely stable identifier.