Design System

Forms & input groups

How the V2 controls are combined into complete forms: the Form foundation, FormField, field groups, input groups, sections, actions, validation, read-only and disabled fields, long forms and the multi-step foundation. Nothing here redesigns a control — it only composes them.

What is this?

A composition layer over the existing V2 controls: Form, FormHeader, FormSection, FieldGroup, FormLayout, FormField, ConditionalField, FormActions and the useMultiStepForm foundation.

When to use it

  • Any screen that collects or edits data — page forms, dialog forms, drawer forms, settings.
  • Multi-section records such as Create resident or Edit care plan.
  • Review screens that mix editable and read-only information.

How to use it

  • Wrap fields in Form and pick a density once; every section, group and field inherits it.
  • One FormField per control: label, control, then helper or validation message.
  • Group only logically related fields (first/last name, city/state/postal code).
  • Put actions after the final field; go sticky only in long forms and overlay footers.

What to avoid

  • New label styles, spacing values, controls or feedback components for a single form.
  • Multi-column layouts for long or unrelated fields.
  • Errors shown before the user has had a fair chance to complete the field.

Form foundation

Structure, rhythm and density for every V2 form.

Form

`<form>` wrapper that owns the vertical rhythm, the density scale and the submitting state. Density flows through context, so a form is spaced consistently without any per-field spacing overrides.

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

When to use

  • Every form, from a two-field dialog to a multi-section record.
  • Whenever every control needs disabling at once, e.g. while submitting.

When not to use

  • A single search box in a toolbar — use SearchInput on its own.
  • Read-only detail pages with no editable control — use DescriptionList.

Density

Comfortable, default and compact — all three come from the 4px spacing scale. Default is the V2 baseline.

Contact

Same fields, three rhythms.

Used for the family portal.

Form density scale
DensityBlock gapField gapLabel → controlUse for
Comfortable40px24px8pxOnboarding, low-frequency forms
Default32px20px6pxThe V2 baseline for page and record forms
Compact20px12px4pxDialogs, drawers, filter panels, settings rows

Accessibility

  • Renders a real <form>; disabled uses a native <fieldset disabled> so browsers and assistive tech understand it without extra ARIA.
  • FormHeader renders a heading, so a form announced in a dialog or page keeps its place in the heading outline.

FormField

One control, wired: label above, control, then helper text or a validation message. Generates ids and links label, description and message to the control via htmlFor, aria-describedby and aria-invalid.

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

When to use

  • Every control that needs a label, helper text or validation.
  • Controls whose own label props are not enough — checkbox rows, custom composites.

When not to use

  • Around a control that already renders its own label (Select, DatePicker, InputGroup) — use that control's label prop instead.

States

Required, optional, helper text, error, warning, success, read-only and disabled.

Used for family portal invitations.

Warning: This number has not been verified.

Success: Matched to an existing record.

Generated when the record is created.

Available once finance approves the placement.

Read-only information

ReadOnlyField presents a value as content, not as a broken control. Read-only means “view but not edit”; disabled means “this control is unavailable”.

Resident

Margaret Ellis

Admitted

12 March 2026

Set by the admissions team.

Accessibility

  • Label sizing is the shared V2 form-label token (14px) — never restyled per form.
  • Required is conveyed with an asterisk plus a visually hidden “(required)”, and by the control's required attribute.
  • Validation messages pair an icon and a hidden status word with the text, so status never depends on colour.
  • Errors are announced with role="alert"; success and warning use a polite status region.

FieldGroup

Groups closely related fields side by side and collapses them to a single column on mobile. Two columns by default, three for short address-style fields.

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

When to use

  • First name / last name, start date / end date, city / state / postal code.
  • Fields a user reads as one answer.

When not to use

  • Unrelated controls placed side by side only to shorten the page.
  • Long inputs such as notes, addresses or rich text.

Name group

Address group (3 columns)

Form layout

FormLayout is the responsive field grid for a whole form body; FormLayoutFull spans a field across the row.

Accessibility

  • Columns collapse below the sm breakpoint so labels and controls stay full width and never clip.
  • Grouping is visual only — every field keeps its own label and validation message.

Input groups, prefixes & suffixes

Several controls that read as one field. Prefix and suffix content is text, not an icon — V2 inputs still carry at most one supporting icon, on the right.

InputGroup

Joins a field with prefixes, suffixes, selects and buttons inside one border, one height and one focus ring.

import { InputGroup, InputGroupField, InputGroupText, InputGroupSelect, InputGroupButton } from "@/design-system";

When to use

  • A unit or symbol that belongs to the value: ₹, %, https://, minutes.
  • A control that qualifies the value, such as a currency or a domain select.
  • A single action bound to the field, such as Verify or Copy.

When not to use

  • Decoration — never add a prefix icon just to fill the space.
  • Unrelated actions; those belong in the form action row.

Prefix and suffix

₹

Excludes optional activities.

%
https://
minutes

Input + select and input + button

Accessibility

  • The group's label targets the inner field via htmlFor, so the whole composite has one accessible name.
  • Prefix and suffix text is presentational; anything essential is repeated in the label or helper text.
  • Buttons inside the group are real buttons with visible focus and an accessible name.

Sections, dependencies & actions

How long forms stay legible and how fields react to each other.

FormSection

A titled block of a longer form: title, optional description, fields and an optional divider. Sections carry the structure so extra boxes, cards and rules are unnecessary.

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

When to use

  • Any form with more than roughly six fields.
  • Records that combine distinct topics — resident details, address, care.

When not to use

  • Short forms where a single group of fields already reads clearly.
  • As a substitute for a multi-step flow that genuinely needs staged validation.

Dependent fields

Country → state and Service → service group. The dependent control stays visible and disabled with helper text — context is never removed.

Placement

Dependent fields keep their context.

Select a country first.

Conditional fields

Fields that appear from a choice reveal in place, are announced politely, and use the standard motion tokens.

Accessibility

  • Each section renders a real heading, keeping the document outline navigable by screen reader.
  • Dividers are decorative; the heading carries the structure.

FormActions

The action row for a form: primary action last, secondary and cancel beside it, destructive actions separated. Buttons go full width on mobile so they stay reachable.

import { FormActions, Button } from "@/design-system";

When to use

  • After the final field of a standard form.
  • As a sticky footer only in long forms and dialog/drawer footers.

When not to use

  • Floating the actions above content in short forms.
  • Duplicating the primary action at both the top and bottom of a form.

Standard placement

Back / continue and destructive

Accessibility

  • Actions are ordinary Buttons: keyboard reachable, visible focus, and a loading state that announces progress.
  • The sticky footer keeps a solid background so focused controls behind it are never obscured.

Multi-step foundation

useMultiStepForm owns step state, completion and per-step status for the existing Stepper. It renders nothing itself, so a wizard is built from the same Form, FormSection, FormField and Button components as a single-page form.

import { useMultiStepForm, Stepper, Step } from "@/design-system";

When to use

  • Workflows that genuinely need staged validation before the next stage is meaningful.

When not to use

  • Long but linear forms — use sections instead of steps.
  • Two-field forms in a dialog.

Three-step onboarding

  1. 1Current step

    Resident

    Who is being onboarded

  2. 2Upcoming

    Care plan

    Service and schedule

  3. 3Upcoming

    Review

    Confirm and submit

Resident

Who is being onboarded

Accessibility

  • Step status is conveyed by icon and text as well as colour; the current step carries aria-current="step".
  • Back and Continue are ordinary buttons, so keyboard users move through steps in reading order.

Validation & lifecycle

When errors appear, where they appear, and how completion is confirmed.

Validation timing
KindWhen it runsHow it is shown
RequiredOn submit, and on blur once the field has been touchedFormField error message
Format (email, phone, postcode)On blur, then live once the field is in errorFormField error message
Range (dates, numbers)On change, because the control constrains the valueFormField error or warning
Cross-field (start ≤ end)On submit and whenever either field changes after touchError on the field the user can fix
ServerOn submit responseAlert above the form, plus field errors where the server maps them

Form states

The complete lifecycle: default, filled, validation error, submitting, success and server error — all built from existing V2 feedback components.

import { Alert, SuccessState, useToast } from "@/design-system";

When to use

  • Toast for a simple save that leaves the user in place.
  • SuccessState for a completed workflow that replaces the form.
  • Alert above the form for a server failure the user can retry.

When not to use

  • Form-specific success banners or bespoke error boxes.
  • Blocking errors shown before the first submit or blur.

Lifecycle

Default

No feedback until the user has had a chance.

Filled

Values entered, validation satisfied.

Success: Email verified.

Validation error

Message sits beside the field it blocks.

Submitting

The form is locked while the request is in flight.

Accessibility

  • Errors are associated with their field and announced assertively; the server Alert is a status region.
  • The submitting state disables the whole form via a native fieldset and the primary Button announces its loading label.

Forms in overlays

Dialog and drawer forms use the same system — only the density and the footer change.

Form in dialog & drawer

Overlay forms reuse Dialog/Drawer, FormField, the existing controls, Button and the same validation. The form body scrolls; the footer holds the actions and never covers a field.

import { Dialog, DialogContent, Drawer, DrawerContent, Form } from "@/design-system";

When to use

  • Short, focused edits that keep the user in context.
  • Side-by-side editing while a list or record stays visible (drawer).

When not to use

  • Long, multi-section records — those belong on a page.
  • Anything that needs a stepper inside a small dialog.

Dialog form

Drawer form

Accessibility

  • Both overlays guard dismissal, so an accidental Escape or outside click cannot discard edits.
  • The submit button is linked to the form with the form attribute, so Enter still submits from any field.
  • The drawer body scrolls independently of the footer, keeping actions reachable without covering fields.

Complete example — Create resident

One realistic Centrim Life form using only V2 components: FormField, Input, Select, DatePicker, field groups, dependent fields, validation, required/optional and Button actions.

Create resident

Records the information needed before a resident can be scheduled for care.

Resident information

Basic information about the resident.

Used for family portal invitations.

Address

Where care is delivered.

Care

Service allocation and handover notes.

Select a service first.

Visible to the care team on every visit.

Why it is designed this way

Forms are the highest-traffic surface in Centrim Life, so the system standardises the combination, not the controls. Density, rhythm and label sizing come from context and tokens, which means a dialog form and a full-page record read as the same product without any per-screen spacing decisions.

Validation deliberately waits: nothing is flagged before submit or blur, and once a field is in error it validates live so the user sees their fix land. Errors sit against the field they block; only failures the user cannot attribute to a field — server errors — surface above the form.

Do

  • Set density once on Form and let sections, groups and fields inherit it.
  • Keep read-only values legible as content, and reserve the disabled style for unavailable controls.
  • Keep dependent fields visible and disabled with helper text explaining what to answer first.
  • Place actions after the final field; primary action last, cancel as a ghost button.

Don't

  • Do not introduce a second label style, spacing value or feedback component for one form.
  • Do not group unrelated fields into columns to shorten the page.
  • Do not show validation errors before the user has had a fair chance to complete a field.
  • Do not add a sticky footer to a form that fits on one screen.