Design System

Data display

Components for presenting structured records — tables, filters, lists, metrics, timelines and trees — plus the file handling layer. Every example renders the real production component.

Table

Low-level table primitives for hand-built tables: Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell and TableCaption.

import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/design-system";

When to use

  • The columns and rows are known up front and need no sorting or filtering.
  • You need full control over the markup inside cells.
  • A summary row belongs in a TableFooter.

When not to use

  • The dataset needs sorting, search, selection or pagination — use DataTable.
  • The content is a set of label/value pairs — use DescriptionList.
  • There are only two columns of loose metadata — use KeyValue.

Row states

Default, hover, selected and disabled rows.

Team seats and their state
MemberRoleSeats
Aria WhitfieldOwner1
Malik OseiEditor1
Freya LindqvistDeactivated0
Selected and disabled rows are also exposed to assistive technology.

Density

Comfortable is the default; compact stays readable and keeps usable touch targets.

compact

compact density example
PlanSeatsMonthly
Starter3£29
Growth12£99
Total£128

default

default density example
PlanSeatsMonthly
Starter3£29
Growth12£99
Total£128

comfortable

comfortable density example
PlanSeatsMonthly
Starter3£29
Growth12£99
Total£128

Accessibility

  • TableHead renders a real <th scope="col"> and supports aria-sort.
  • Every table needs a caption — pass Table's caption prop or render TableCaption.
  • Row separators are horizontal only; state is never conveyed by colour alone (selected rows are also exposed with data-selected, disabled rows with aria-disabled).
  • The horizontal scroll wrapper keeps wide tables reachable by keyboard on small screens.

DataTable

A generic, typed data table built on the Table system: sorting, search, column filters, row selection with a BulkActionBar, Pagination, row actions, expandable rows and the shared loading/empty/no-results/error states.

import { DataTable, type DataTableColumn } from "@/design-system";

When to use

  • A dataset needs any combination of sorting, filtering, selection or pagination.
  • Rows carry per-row actions or expandable detail.
  • The same table has to express loading, empty, no-results and error states.

When not to use

  • The table is static — use the Table primitives directly.
  • You are showing a single record's attributes — use DescriptionList.
  • The dataset is a short, scannable set of entities with avatars — a List reads better.

Sortable, filterable, selectable

Sort a column, search, filter by status, select rows and expand a row for detail.

Customer invoices
Expand row
StatusActions
INV-1042
AW

Aria Whitfield

aria@northfern.example

£4,200.00Paid
INV-1043
MO

Malik Osei

malik@brightloop.example

£1,180.00Pending
INV-1044
FL

Freya Lindqvist

freya@nordvik.example

£860.00Overdue
INV-1045
DS

Diego Salgado

diego@cantera.example

£3,260.00Paid
INV-1046
PC

Priya Chandran

priya@ambervale.example

£990.00Pending

Showing 1–5 of 10 invoices

Loading

Skeleton rows reuse the existing Skeleton component.

Customer invoices
Expand row
StatusActions

Empty

No data exists yet — renders EmptyState. Searching with no matches renders NoResultsState instead.

Customer invoices
Expand row
StatusActions

Nothing here yet

There is nothing here yet.

Error

Loading failed — renders ErrorState with a retry action.

Customer invoices
Expand row
StatusActions

Something went wrong

The invoices service returned an error. Try again.

Responsive

Low-priority columns are hidden below their breakpoint (hideBelow) and the table scrolls horizontally rather than shrinking text.

Narrow viewport invoices
Status
INV-1042
AW

Aria Whitfield

aria@northfern.example

£4,200.00Paid
INV-1043
MO

Malik Osei

malik@brightloop.example

£1,180.00Pending
INV-1044
FL

Freya Lindqvist

freya@nordvik.example

£860.00Overdue

Showing 1–3 of 3 rows

Accessibility

  • Sortable headers are real buttons that set aria-sort and announce the current and next sort order.
  • Sort direction uses distinct arrow icons plus text, never colour alone.
  • The header checkbox exposes an indeterminate state; every row checkbox has a per-row accessible name.
  • Selection count is announced politely through the BulkActionBar; the pagination summary is an aria-live region.
  • Expand controls set aria-expanded; row actions are IconButtons with per-row labels.

FilterBar

Toolbar that groups a search field, filter controls and active-filter chips above a data set. Filters sit inline from md up and collapse behind a Filters toggle below that.

import { FilterBar } from "@/design-system";

When to use

  • A table or list has two or more filter dimensions.
  • Users need to see and remove what is currently applied.
  • Filtering is expensive and needs an explicit Apply action.

When not to use

  • There is a single search box and nothing else — render SearchInput on its own.
  • The filters belong to a form submission — use the Form components.

Search, filters and chips

Resize the preview to see the filters collapse behind the Filters toggle.

Accessibility

  • The bar is a role="search" region with an accessible name.
  • The mobile toggle sets aria-expanded and shows the active filter count in a Badge.
  • Each chip has a per-filter remove button with a descriptive label.

BulkActionBar

Contextual bar shown while rows are selected: the selected count, a primary action, secondary actions, a separated destructive action and clear selection.

import { BulkActionBar } from "@/design-system";

When to use

  • A table or list supports multi-select and actions apply to the selection.
  • Users need a fast way to clear the current selection.

When not to use

  • Actions apply to a single row — use row actions instead.
  • The action is a page-level primary action — put it in the page header.

With a selection

3 invoices selected

Accessibility

  • The bar is a labelled region and the count is announced with aria-live="polite".
  • Actions are the existing Button/IconButton components, so focus and contrast are inherited.
  • The destructive action is separated visually and by wording, never by colour alone.

List

A vertical list of records: List, ListItem, ListHeader, ListContent and ListActions, with leading/trailing slots for Avatar, Badge and action buttons.

import { List, ListItem, ListHeader, ListContent, ListActions } from "@/design-system";

When to use

  • Entities are best scanned as rows of a name plus supporting metadata.
  • The presentation must stay readable on narrow screens where a table would not.
  • Each row has one or two actions.

When not to use

  • Records need to be compared across many columns — use a Table or DataTable.
  • The content is a single record's attributes — use DescriptionList.

States and slots

  • Workspace members4 members
  • AW

    Aria Whitfield

    Owner

    Last active 2 hours ago

    Active
  • MO

    Malik Osei

    Editor

    Last active yesterday

    Selected
  • Support rota

    Group of 6

    Shared inbox

  • FL

    Freya Lindqvist

    No longer has access

    Deactivated

Accessibility

  • Renders real <ul>/<li> elements.
  • Interactive items are focusable with a visible focus ring and activate with Enter/Space.
  • Selected items set aria-selected; disabled items set aria-disabled and are not focusable.

DescriptionList

Semantic dl/dt/dd for a labelled record summary, in horizontal or vertical layout.

import { DescriptionList, DescriptionItem } from "@/design-system";

When to use

  • Showing the attributes of one record: status, owner, created date.
  • A detail panel or drawer needs a compact summary block.

When not to use

  • Comparing several records — use a Table.
  • There is exactly one fact to show — use KeyValue or Stat.

Horizontal

Status
Active
Created
20 August 2026
Owner
Aria Whitfield

Vertical grid

Company
Northfern Ltd
VAT number
GB 928 4471 02
Billing email
billing@northfern.example
Country
United Kingdom

Accessibility

  • Uses real <dl>, <dt> and <dd> elements so terms and values are programmatically associated.
  • Horizontal items stack below sm so long values never truncate on mobile.

KeyValue

Lightweight label/value pair with an optional description, icon and loading state. Deliberately surface-less.

import { KeyValue } from "@/design-system";

When to use

  • Dense metadata rows inside a panel, popover or card you already have.
  • A value is fetched separately and needs a placeholder while loading.

When not to use

  • The pair deserves its own card — it almost never does.
  • Several related attributes belong together semantically — use DescriptionList.

Metadata block

InvoiceINV-1042
Issued12 January 2024
Due26 January 2024Net 14 payment terms
Balance

Accessibility

  • Plain text rows; group several inside a labelled section so the context is announced once.

Metric

A labelled numeric value with an optional description, icon and trend delta communicated by icon, text and colour together. MetricGroup lays several out responsively.

import { Metric, MetricGroup } from "@/design-system";

When to use

  • Summarising a handful of headline numbers above a data set.
  • A number changes over time and the direction matters.

When not to use

  • There is a single KPI with supporting copy — use Stat.
  • The number is one attribute among many — use DescriptionList.

Group

Total users12,480Increase: 8.4%
Monthly revenue£48,290Increase: 4.2%Last 30 days
Churned accounts6Decrease: 1.1%
Active seats1,204No change: No change

Sizes

Compact482
Default4,820
Large48,290

Accessibility

  • Delta direction is never colour-only — an arrow icon plus text (e.g. "4.2%") always accompanies it.
  • The direction is also announced in words ("Increase", "Decrease") for screen readers.

Stat

A restrained KPI readout: label, primary value, supporting text, trend and an optional comparison. Hierarchy comes from typography and spacing, not coloured panels.

import { Stat } from "@/design-system";

When to use

  • A dashboard header needs one or two headline figures with context.
  • The figure benefits from a comparison baseline ("vs. previous period").

When not to use

  • You need four evenly weighted numbers in a row — use MetricGroup.
  • The figure needs a coloured hero treatment — that is a product decision, not a system one.

With and without a card

Monthly recurring revenue

£48,290

Increase: 8.4% vs. previous period

Updated 5 minutes ago

Open support tickets

37

Increase: 12 more than last week

Accessibility

  • Trend uses an icon, a direction word for screen readers and text — never colour alone.
  • No background colour is applied, so contrast is inherited from the surface it sits on.

Timeline

Vertical sequence of dated events with a connecting line and status markers: completed, current, upcoming and error.

import { Timeline, TimelineItem } from "@/design-system";

When to use

  • A record has an ordered history: an order, an approval chain, an audit trail.
  • Each event has a time, a title and optional detail.

When not to use

  • The steps are a form flow the user is completing — use Stepper.
  • The entries are actor-driven social activity — use ActivityFeed.

Order history

  1. Completed: Order placed

    Order #4821 confirmed.

    Automated

  2. Completed: Payment captured

    Charged the card on file.

  3. Error: Address check failed

    Delivery postcode could not be verified.

  4. Current: Preparing shipment

    Packed at the northern warehouse.

  5. Upcoming: Out for delivery

    Expected by end of day.

Accessibility

  • Status is announced as text before the title, so markers are never colour-only.
  • Each status has a distinct icon (check, dot, ring, warning).
  • Timestamps use a real <time> element.

ActivityFeed

Chronological actor + action log with avatar or icon, timestamp, metadata, optional quoted content and an optional trailing action.

import { ActivityFeed, ActivityItem } from "@/design-system";

When to use

  • Showing who did what and when: comments, audit trails, notifications.
  • Entries may carry quoted content or a follow-up action.

When not to use

  • The events describe a single record's lifecycle stages — use Timeline.
  • The entries are unread notifications in a menu — use Notification.

Recent activity

  • PC

    Priya Chandran left a comment on invoice INV-1046

    Can we resend this to their new billing contact?
  • Tom Fairweather was added to the Finance workspace

    Access change

  • Invoice INV-1047 marked as overdue

    No payment received 14 days after the due date.

Accessibility

  • Each entry reads as a single sentence: actor, action, then timestamp.
  • Trailing actions are Buttons/IconButtons with per-entry accessible names.

Tree

Hierarchical list with expand/collapse, selection, disabled nodes and full keyboard navigation.

import { Tree, TreeItem } from "@/design-system";

When to use

  • Content is genuinely nested: folders, categories, org structures.
  • Users need to expand only the branch they care about.

When not to use

  • The hierarchy is one level deep — a List with headers is simpler.
  • The tree is site navigation — use Sidebar with sub-items.

File tree

Focus the first node, then navigate with the arrow keys.

  • design-system
    • tokens
      • colour.css
      • spacing.css
  • archive (read only)

Accessibility

  • Uses role="tree", role="treeitem" and role="group" with aria-level, aria-expanded and aria-selected.
  • Roving tabindex: Arrow Up/Down move between visible nodes, Arrow Right expands, Arrow Left collapses or moves to the parent, Home/End jump to the ends.
  • Disabled nodes set aria-disabled and are skipped by keyboard navigation.

FileUpload / Dropzone

FileUpload is a button-triggered hidden file input. Dropzone adds a large drag-and-drop target with a keyboard-activatable browse fallback.

import { FileUpload, Dropzone } from "@/design-system";

FileUpload

Dropzone

Drag files here

PDF, PNG or JPG up to 10MB

Uploads unavailable

Reconnect to continue

Drag files here

Accessibility

  • The hidden input keeps a real accessible name via aria-label / label association.
  • Dropzone is a keyboard-operable button (Enter/Space) as well as a drop target.
  • Disabled and error states are visually distinct and exposed via aria-disabled / aria-invalid.

FileList / FileItem

Shows selected or uploading files with type icon, size, progress bar and success/error states.

import { FileList } from "@/design-system";

Mixed states

  • brand-guidelines.pdf

    2.3 MB

    Uploaded

  • hero-banner.png

    1.0 MB

  • budget-2024.xlsx

    332.0 KB

Accessibility

  • Progress uses role="progressbar" with aria-valuenow.
  • Error rows use role="alert" text alongside the icon.
  • View, download, retry and remove are IconButtons with a per-file accessible name.
  • The action set is revealed on row hover or keyboard focus and stays permanently visible on touch devices.

ImagePreview

Thumbnail preview for an uploaded image with a required alt text, loading spinner and broken-image fallback.

import { ImagePreview } from "@/design-system";

States

Product packaging photo
Broken image example
No image attached

Accessibility

  • alt is a required prop — there is no way to render an unlabelled image.
  • Loading and error states announce via visible icon + text.
See the Forms page for upload progress inside a submission flow.