Form & Auth Patterns
How to compose form fields, validation, and authentication flows with GHDS components.
Overview
Forms are the primary way users give data to an application. This pattern covers composing
FormField, Input, Textarea, Select, Checkbox, Radio, Switch, and Button into
validated, accessible forms — plus the authentication flows (login, signup, password reset) that
form the entry point to most apps.
Form Layout Composition
A well-structured form layers three kinds of information per field, all spaced via sys.spacing.*:
┌─ FormField ──────────────────────────────┐
│ Label (required/optional marker) │ ← comp.formField.label.*
│ ┌─ Input / Select / Textarea ──────────┐ │
│ │ │ │ ← comp.formField.input.*
│ └──────────────────────────────────────┘ │
│ Helper text (contextual, optional) │ ← comp.formField.helper.*
│ Error message (role="alert", conditional)│ ← comp.formField.error.*
└───────────────────────────────────────────┘
FormField automates the wiring: it links the label via htmlFor / for, and connects error
messages to inputs using aria-describedby + aria-invalid, so assistive technology announces
errors as they appear.
Spacing
Every gap between fields, labels, and messages is sourced from sys.spacing.*. The
comp.formField.gap token controls the vertical rhythm between stacked FormField instances;
internal label→input→helper→error gaps are also token-driven.
/* Stack form fields */
.form { display: flex; flex-direction: column; gap: var(--comp-formField-gap); }
Validation
When to Validate
Do
- Validate on blur (per-field) — let the user finish typing first.
- Validate on submit (whole-form) — catch everything at once before the request.
- Announce errors assertively with role="alert" so screen readers interrupt.
- Mark required fields with an asterisk or "(required)" text — not colour alone.
Don't
- Don't validate on every keystroke — it's noisy and punishes fast typists.
- Don't clear errors until the field is re-focused or re-submitted.
- Don't use placeholder text as a label — it disappears on focus.
Inline vs Summary Errors
| Strategy | When to use | Implementation |
|---|---|---|
| Inline (per-field) | Short forms (≤ 5 fields), or when the error is tightly coupled to one field | FormField error="..." |
| Summary (top of form) | Long forms (≥ 6 fields), or when one decision affects multiple fields | Render an Alert variant="danger" above the form summarising all errors, plus inline indicators |
For summary errors, pair a top-level Alert with per-field aria-invalid="true" markers — the
summary announces the error count, and each field shows a visual indicator.
// React — summary + inline pattern
{errors.length > 0 && (
<Alert variant="danger" title={`${errors.length} field(s) need attention`}>
<ul style={{ margin: 0, paddingLeft: '1.25rem' }}>
{errors.map((e) => <li key={e.field}>{e.message}</li>)}
</ul>
</Alert>
)}
<FormField label="Email" error={fieldErrors.email}>
<Input aria-invalid={!!fieldErrors.email} />
</FormField>
Authentication Flows
Login
A minimal two-field form (FormField + Input for email/password, Button variant="primary" to
submit). A “Forgot password?” link navigates to the reset flow. Failed login shows an inline error
or a top-of-form Alert variant="danger".
// React — login form
<FormField label="Email">
<Input type="email" placeholder="you@example.com" />
</FormField>
<FormField label="Password">
<Input type="password" />
</FormField>
<Button variant="primary">Log in</Button>
Signup (Step-by-Step)
Multi-step signup uses Tabs (or sequential panels) to split the form into manageable chunks.
Each step has its own validation; the “Next” button only enables when the current step is valid.
A Progress bar or step indicator shows position.
// React — 3-step signup with Tabs
const [step, setStep] = useState(0);
<Tabs activeTab={step} onChange={setStep} items={[
{ label: 'Account', content: <AccountStep /> },
{ label: 'Profile', content: <ProfileStep /> },
{ label: 'Confirm', content: <ConfirmStep /> },
]} />;
Password Reset
Two screens: (1) enter email → Button submits → Toast variant="success" (“Check your inbox”);
(2) enter new password + confirm → Button submits → redirect to login. On error, Alert variant="danger" with a retry prompt.
Live Demo
React @ghds/react
Web Components @ghds/web-components
// React Native — login form
import { Button } from '@ghds/react-native/button';
import { FormField } from '@ghds/react-native/form-field';
import { Input } from '@ghds/react-native/input';
<FormField label="Email">
<Input placeholder="you@example.com" />
</FormField>
<FormField label="Password">
<Input secureTextEntry />
</FormField>
<Button label="Log in" variant="primary" onPress={handleLogin} />
Accessibility
| Key | Action |
|---|---|
Tab | Move between form fields in document order. |
Enter | Submit the form (when focus is in a field). |
Space | Toggle Checkbox / Switch; activate Button. |
- Error announcement:
FormFieldwiresaria-describedbyto the error, and the error element hasrole="alert"so it interrupts screen readers. - Required fields: Mark with text (“required”) rather than only colour.
FormFieldaccepts arequiredprop that appends an asterisk — visually and for screen readers. - Disabled buttons: A disabled submit button communicates “form not ready” — pair it with visible hints about which fields need attention.
- See the Accessibility Guide for full keyboard and ARIA reference.