FormField

A Label + HelperText + ErrorText composition wrapper for a single form control.

Overview

FormField composes a Label + HelperText + ErrorText around a single form control, auto-wiring aria-describedby/aria-invalid where the platform allows it. It’s the foundation for the upcoming M12 form-patterns milestone — reach for it whenever a control needs a label, a helper message, or an error, instead of hand-rolling the id/ARIA wiring per screen.

On React, don’t also pass label/error directly to a wrapped Input/TextareaFormField owns rendering those; the wrapped control reads its id and ARIA attributes from context instead.

Anatomy

No sketchy box — FormField is pure layout and typography. It renders (in order): an optional label, the wrapped control (via children on React/React Native, or a <slot> on Web Components), an optional helper-text line, and an optional error line. If both helperText and error are supplied, both render simultaneously — there is no hide-on-error switching.

Variants & States

State Status Notes
Label only Implemented
Helper text Implemented
Error Implemented Rendered with role="alert".
Helper text + error together Implemented Both render at once — no hide-on-error switching.
No label/helper/error Implemented Renders only the wrapped control — a no-op decoration.

Usage

Do

  • Use FormField to wrap a single Input/Textarea that needs a label, helper text, or an error.
  • On React Native, use FormField's label to give Input a label (it has none of its own) — omit it when wrapping a control that already renders its own label (Textarea, Checkbox, Radio, Switch, Select, Slider).
  • Prefer passing label/error to FormField itself on React rather than the wrapped control — the wrapped control still renders its own if FormField was not given one, so a control-level label/error is never silently lost.

Don't

  • Don't wrap more than one control in a single FormField on any platform — it composes around exactly one.

Accessibility

This is the single most important section — the wiring mechanism is genuinely different on each platform:

Content

The label names the value being collected (“Email”), not an instruction. Helper text explains a format or constraint before the user makes a mistake (“Letters, numbers, and underscores only”); error text explains what went wrong and how to fix it, after validation.

Props API

React@ghds/react

PropTypeDefaultDescription
labelstringVisible label for the wrapped control.
helperTextstringNon-error descriptive text, always shown regardless of error state.
errorstringError message. When set, the wrapped control is marked invalid.
idstringId applied to the wrapped control (and used to derive helper/error ids).
childrenReactNodeA single form control — reads id/aria-invalid/aria-describedby from context.

Web Components@ghds/web-components

PropTypeDefaultDescription
forstringId of the wrapped control — mirrors native <label for>. Auto-assigned to the slotted control if it has none.
labelstringVisible label for the wrapped control.
helper-textstringNon-error descriptive text (property: helperText).
errorstringError message. Rendered with role="alert" when set; also sets aria-invalid on the slotted control.
helperIdstring (readonly getter)Computed as `${baseId}-helper`, where `baseId` is `for` if set, otherwise an internally generated id.
errorIdstring (readonly getter)Computed as `${baseId}-error`, where `baseId` is `for` if set, otherwise an internally generated id.

React Native@ghds/react-native

PropTypeDefaultDescription
labelstringVisible label. Primarily useful for wrapping Input, which has no label prop of its own.
helperTextstringNon-error descriptive text, always shown regardless of error state.
errorstringError message, shown alongside helperText if both are set.
childrenReactNodeA single form control.
testIDstringTest handle for queries.

Code examples

// React — @ghds/react (wraps one control; label/error render on FormField)
import { FormField } from '@ghds/react/form-field';
import { Input } from '@ghds/react/input';

<FormField label="Password" error="Must be at least 8 characters">
  <Input type="password" />
</FormField>;
<!-- Web Components — @ghds/web-components (auto-wires id/aria onto the slotted control) -->
<script type="module">
  import '@ghds/web-components/form-field';
</script>
<gh-form-field for="password" label="Password" error="Must be at least 8 characters">
  <input type="password" />
</gh-form-field>
// React Native — @ghds/react-native (visual composition only; no ARIA linkage on this platform)
import { FormField } from '@ghds/react-native/form-field';
import { Input } from '@ghds/react-native/input';

<FormField label="Email" error={emailError}>
  <Input value={email} onChangeText={setEmail} />
</FormField>;

Live Demo

React and Web Components render live below. React Native can’t run in a browser without additional react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.

React @ghds/react

Web Components @ghds/web-components