Design System

Forms & advanced inputs

Structural form primitives (Form, FormField, FieldSet, FormSection) and the advanced input composites built on top of them — search, numeric, password, combobox, multi-select, date/time pickers, sliders and toggle groups.

Form

`<form>` wrapper that establishes the standard vertical rhythm between fields and sections, and can disable every descendant control at once (e.g. while submitting) via a native `<fieldset disabled>` boundary.

import { Form, FormField, FieldGroup, FieldSet, FormSection } from "@/design-system";

Account details form

FormField wires label, description and validation message to each control automatically.

Company profile

This information appears on invoices and shared reports.

As shown on Companies House.

Notifications

Choose how the finance team hears about invoices.

Accessibility

  • Uses a real <form> element; the disabled prop disables controls via a native <fieldset>, which browsers and assistive tech understand without extra ARIA.
  • Vertical spacing is consistent so screen magnifier and low-vision users get predictable, scannable structure.

FormField

Connects a visible label, optional description and a single validation message (error, success or warning) to exactly one control, generating and wiring all the ids for you.

import { FormField, FormLabel, FormDescription, FormMessage } from "@/design-system";

States

Only one of error / success / warning should be set at a time.

Visible to everyone in the workspace.

Success: Endpoint responded with 200 OK.

Warning: You're close to your plan limit (18/20 used).

FormLabel, FormDescription and FormMessage standalone

Used directly when you need custom layout instead of FormField.

Rotate this key if it may have been exposed.

Warning: Last rotated 214 days ago.

Accessibility

  • Sets aria-describedby on the control to point at the description and message.
  • Error messages set aria-invalid on the control automatically.
  • Use one FormField per control — never nest two controls inside a single FormField.

FieldGroup, FieldSet & FormSection

Layout helpers for longer forms: FieldGroup places closely related fields side by side, FieldSet groups controls under a native legend, and FormSection titles a whole segment of the form.

import { FieldGroup, FieldSet, FormSection } from "@/design-system";

FieldGroup

Two related fields side by side on wide screens, stacked on narrow ones.

FieldSet

Delivery address

Used for physical invoices and hardware shipments.

Accessibility

  • FieldSet renders a real <fieldset>/<legend> pair, so assistive tech announces the group name before each control.
  • FormSection uses a heading element so the section is reachable from a screen reader's headings list.

SearchInput

A text field dedicated to search: leading icon, clear affordance once there's a value, and a loading state for async results.

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

Sizes

Accessibility

  • role=’searchbox’ is set automatically.
  • The clear button always has an accessible name (’Clear search’).

NumberInput

A numeric field with min/max/step, stepper buttons and native keyboard arrow support — for quantities, prices and other bounded numbers.

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

States

Accessibility

  • role=’spinbutton’ with aria-valuenow/min/max kept in sync.
  • Stepper buttons are disabled at the min/max bounds rather than silently doing nothing.

PasswordInput

A password field with a show/hide visibility toggle that announces its pressed state, for sign-in and account forms.

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

States

Strong password.

Accessibility

  • The toggle uses aria-pressed and an accessible name that changes with state (’Show password’ / ’Hide password’).

Combobox

A single-select typeahead field for choosing one value from a longer list, such as a country or a customer — searchable rather than a long native <select>.

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

Country

Type to filter, use arrow keys and Enter to select.

Accessibility

  • role=’combobox’ with aria-expanded on the trigger; the popover list is keyboard-navigable via cmdk.
  • The clear affordance is reachable and operable with Enter/Space.

MultiSelect

Like Combobox, but for choosing several values at once; selections render as removable tags inside the trigger.

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

Skills

Selected items appear as removable tags.

Product designFrontend engineering

Accessibility

  • Each selected tag has its own ’Remove {label}’ accessible name.
  • The trigger keeps role=’combobox’ so its behaviour matches Combobox for assistive tech users.

DatePicker

A single-date field: a button trigger showing the formatted date, opening a keyboard-navigable calendar popover on activation.

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

Project start date

Past dates are disabled.

When work on this project begins.

States

Accessibility

  • Built on the existing Popover + Button composites, so focus management and Escape-to-close behaviour is inherited from Radix Popover.
  • The calendar grid supports arrow-key navigation and Enter to select, per the react-day-picker accessibility model.

DateRangePicker

Selects a start and end date together — a booking stay, a reporting period, a dashboard time filter. Always labelled: the visible label defaults to “Date range”, so the trigger is never ambiguous outside its surrounding context.

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

When to use

  • Any two-ended period: reporting windows, stays, campaign schedules.
  • Dashboard filters — pair with hideLabel only when a heading already names the filter.

When not to use

  • A single date — use DatePicker.
  • A time of day — use TimePicker.

Default

No props — the label defaults to “Date range”.

Custom label and range selected

With description

Metrics are recalculated for the selected window.

States

Hidden visible label

Use only when a nearby heading already names the control; the accessible label remains.

Accessibility

  • The visible label is a real <label> bound to the trigger; hideLabel keeps it screen-reader-only, never removed.
  • The trigger's accessible name includes the selected range, so the current value is announced.
  • Month navigation uses IconButton with explicit “Previous month” / “Next month” names.
  • Keyboard: Enter/Space opens, Escape closes, arrow keys move across dates, disabled dates are skipped and announced as disabled.

TimePicker

An accessible time selector presenting a listbox of time slots at a configurable step, in 12- or 24-hour format — for scheduling meetings or deliveries.

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

Meeting time

15-minute increments, 24-hour clock.

12-hour format

Accessibility

  • The trigger uses role=’combobox’; the popover list uses role=’listbox’/’option’ with aria-selected kept in sync.

Slider

A single-thumb slider for choosing one value within a bounded range, e.g. a budget or a volume level.

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

Sizes

With value label and error state

£4,500
95%
40

Accessibility

  • Built on Radix Slider: arrow keys, Home/End and Page Up/Down all move the thumb.
  • Always give it a visible label or an aria-label.

RangeSlider

A two-thumb slider for selecting a numeric range, such as a price band or an age bracket.

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

Price range

£200 – £800

Accessibility

  • Each thumb has its own accessible name via thumbLabels (defaults to ’Minimum’/’Maximum’).
  • Thumbs cannot cross, preventing an invalid min > max range.

ToggleGroup

A set of toggle buttons that are either mutually exclusive (type=’single’, e.g. text alignment) or independently toggled (type=’multiple’, e.g. notification channels).

import { ToggleGroup, ToggleGroupItem } from "@/design-system";

Single selection — text alignment

Multiple selection — text formatting

Text labels — notification channels

type=’multiple’ lets more than one item be on at once.

email

Accessibility

  • Uses Radix Toggle Group: roving tabindex, so arrow keys move focus between items with a single Tab stop for the group.
  • Give each ToggleGroupItem an accessible name — text content, or aria-label for icon-only items.

Form layouts

Reusable layout patterns for form bodies: single-column, two-column, compact and inline. FormLayout is a responsive grid; FormActions is the footer row for submit/cancel.

import { FormLayout, FormLayoutFull, InlineForm, FormActions } from "@/design-system";

When to use

  • Single column (columns={1}) for anything the user reads top-to-bottom — the safest default.
  • Two columns (columns={2}) for short, closely-related fields on wide screens; they stack below md.
  • InlineForm for one-line forms such as a search bar or an invite field with a button.
  • density='compact' inside dense surfaces like drawers, filter panels and settings rows.

When not to use

  • Do not use multi-column layouts for long or error-prone fields — scanning across columns hurts completion.
  • Do not nest FormLayout inside FieldGroup; pick one grid per row.

Two-column with a full-width field

Shown on invoices.

Compact density

density='compact' tightens the rhythm for dense surfaces.

Inline form

Side by side from sm up, stacked and full width below it.

Form actions

Accessibility

  • Column order follows DOM order, so the tab sequence always matches the visual reading order.
  • Controls go full width rather than shrinking on small screens.

File upload in forms

FileUpload, Dropzone and FileList/FileItem composed inside the form field system, with upload progress rendered by the existing Progress component.

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

When to use

  • FileUpload when the upload is secondary to the rest of the form.
  • Dropzone when uploading is the primary action on the surface.
  • FileList to show what has been attached, with per-file progress, retry and remove.

When not to use

  • Do not build a bespoke upload widget — compose these three instead.

Attachments field

PDF or PNG up to 10MB. Files upload as soon as they are added.

Drag files here

or browse from your device

  • invoice-2026-04.pdf

    242.2 KB

    Uploaded

  • contract-draft.docx

    1.2 MB

  • scan.png

    4.6 MB

Error state

Drag files here

Accessibility

  • Drag state is communicated by a label change and border treatment, not colour alone.
  • Progress rows expose role=progressbar with aria-valuenow; failures render role=alert text.
  • Remove and retry are IconButtons with a per-file accessible name; view and download join them as neutral actions.

Textarea

Multi-line text field for longer free-form content such as comments or descriptions, shown here inside a FormField for context.

import { Textarea, FormField } from "@/design-system";

Project notes

Visible to everyone with access to this project.

Accessibility

  • Shares the same label/description/message wiring as every other FormField control.

Complete form example

This example demonstrates how individual form components can be composed into a complete, production-ready form. Use it as a reference for layout, hierarchy, spacing, validation and responsive behaviour.

PageHeaderContainerFormFormSectionFormLayoutFormFieldInputSelectComboboxSwitchRadioGroupDatePickerTextareaMultiSelectDropzone / FileListButtonAlertDialogSuccessStateErrorState

When to use this pattern

Use this structure for forms that contain multiple related groups of information.

Form anatomy

  1. 1.Page header — breadcrumb, title and description set the context.
  2. 2.Form sections — Personal information, Preferences, Additional information.
  3. 3.Field groups — two columns for short fields, full width for long ones.
  4. 4.Validation — messages sit directly under the field they belong to.
  5. 5.Actions — Cancel and the primary Save profile, in a footer row.

Do

  • Group related fields.
  • Use clear labels.
  • Keep forms visually scannable.
  • Use helper text only when necessary.
  • Keep primary actions clear.
  • Use responsive layouts.
  • Show validation close to the affected field.

Don't

  • Put unrelated fields together.
  • Use placeholder text as the only label.
  • Overuse cards.
  • Create unnecessary sections.
  • Use colour as the only validation indicator.
  • Create custom components when an existing design-system component works.

Playground

How the form looks before anything is submitted.

Create profile

Add the basic information and preferences for this profile.

Personal information

Enter the basic details for this profile.

We'll use this address for account-related communication.

Success: Email address verified.

Preferences

Choose the settings that apply to this profile.

Send activity updates for this profile.

Communication preference
Updates arrive in the inbox.
Short notifications only.
Suppress all outbound messages.

How this profile prefers to be contacted.

Additional information

Add optional information where useful.

Add any additional information that may help identify this profile.

Onboarding

Upload PDF, PNG, or JPG files.

Drag files here

or browse from your device

  • identity-verification.pdf

    474.6 KB

    Uploaded

  • profile-photo.png

    125.4 KB

    Uploaded