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

StrategyWhen to useImplementation
Inline (per-field)Short forms (≤ 5 fields), or when the error is tightly coupled to one fieldFormField error="..."
Summary (top of form)Long forms (≥ 6 fields), or when one decision affects multiple fieldsRender 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

Admin Editor Viewer Log in
// 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.