Form Control
A wrapper that provides consistent label, helper text, and error messaging for form fields. Use to ensure all form inputs meet layout and accessibility standards. Don't apply custom label or error patterns outside this component.
FigmaDescription
Form Control is the structural wrapper that every form field lives inside. It is not a field itself — it provides the consistent layout layer that surrounds inputs, dropdowns, text areas, checkboxes, and other controls.
Every form in Open Point and Social Point — consultation setup, stakeholder profiles, survey builders, settings screens, login and registration flows, and inline editing contexts.
Without Form Control, each team member who builds a form makes independent decisions about label positioning, error placement, spacing, and ARIA associations. Form Control eliminates that variation. Any field wrapped in it automatically gets the same layout, spacing, and accessibility wiring as every other field in the product.
Anatomy
| Part | Required? | Notes |
|---|---|---|
| Label | Required | Always visible above the field. Programmatically associated with the control via for/id or aria-labelledby. Never substitute placeholder text for a label. |
| Required indicator | Conditional | An asterisk (*) or "Required" text alongside the label when the field is mandatory. The visual indicator must have a text equivalent — not colour alone. |
| Field slot | Required | The interactive control. Can be any field component: Input, TextArea, Dropdown, RadioButton, Checkbox, Switch, etc. |
| Helper text | Optional | Below the field. Provides context that the label alone can't — format hints, length limits, examples. Associated with the field via aria-describedby. |
| Error message | Conditional | Replaces helper text when validation fails. Prefixed with an error icon. Associated via aria-describedby. Set aria-invalid="true" on the field element simultaneously. |
| Character count | Optional | Used with TextArea or constrained inputs. Updates live as the user types. Associated via aria-describedby. |
States
| State | Behaviour |
|---|---|
| Default | Label above, optional helper text below. Field in its resting state. |
| Error | Error message replaces helper text. Red text and error icon. Field receives aria-invalid="true". Triggered on blur or form submission — not on every keystroke. |
| Disabled | Label, field, and helper text all render at reduced opacity. The entire control is non-interactive. Communicate why the field is disabled nearby if the reason isn't obvious. |
Usage guidelines
Required fields
Only mark fields as required when they genuinely are. In a form where most fields are required, consider marking the optional ones instead and noting the convention at the top of the form.
The required indicator (asterisk or "Required") must always be accompanied by a text explanation — either visible near the top of the form ("Fields marked with * are required") or included in the label itself ("Full name (required)"). Never rely on the asterisk or red colour alone.
When to use
- Every form field — no exceptions. Input, TextArea, Dropdown, RadioButton, Checkbox, Switch, and all other form controls should live inside a Form Control wrapper.
- Even fields with unconventional layouts (inline forms, filter bars, search) should use Form Control where possible to preserve the accessibility wiring.
When not to use
- Standalone icon buttons or controls that aren't part of a form — these don't need a label/helper structure.
- Groups of related fields (e.g. a date with separate day/month/year inputs) — use a
<fieldset>and<legend>to wrap the group, with individual Form Controls inside for each field.
Do / Don't
Do
Always use a visible label. Every field must have one — even if the design shows a clean, label-free layout, the label must exist in the DOM and be associated with the field.
Don't
Don't use placeholder text as a substitute for a label. Placeholder text disappears the moment the user starts typing, leaving them with no reminder of what the field is for.
Do
Write error messages that tell users exactly what to do: "Enter a valid email address (e.g. name@example.com)" rather than "Invalid input".
Don't
Don't show error messages before the user has interacted with the field. Trigger validation on blur (when leaving the field) or on form submission — not on page load.
Do
Use helper text to set expectations before the user types — format, length, examples. "Must be at least 8 characters" is more useful than waiting for an error.
Don't
Don't use helper text to restate the label. It should add context the label doesn't already provide.
Layout & Spacing
Form Control uses a vertical stack layout. The label sits above the field, helper text and error messages sit below. Spacing is consistent regardless of which field component is inside.
| Element | Spec |
|---|---|
| Label → field gap | --op-space-8 (0.5rem) |
| Field → helper / error gap | --op-space-8 (0.5rem) |
| Between form controls (vertical) | --op-space-24 (1.5rem) |
| Label font size | --op-text-sm |
| Label font weight | 600 |
| Helper text font size | --op-text-sm |
| Error message font size | --op-text-sm |
| Required indicator | Asterisk inline with label, --op-space-4 gap, colour --op-color-status-error |
Tokens
| Part | Token | Value |
|---|---|---|
--op-color-text-primary | ||
--op-color-status-error | ||
--op-color-text-muted | ||
--op-color-status-error | ||
--op-color-status-error | ||
— |
Engineering notes
- The Form Control wrapper is responsible for the
aria-describedbyrelationship — the field component itself should expose anid, and the Form Control generates matching IDs for helper text and error message elements. - Helper text and error messages should not both be present in the DOM simultaneously with active
aria-describedbylinks — toggle visibility at the element level or swap thearia-describedbyvalue. Avoiddisplay: noneon elements still referenced byaria-describedby. - The error message element should carry
role="alert"so it's announced immediately when it appears — this removes the need for the user to refocus the field to hear the error. - Use the
autocompleteattribute on any field where the browser can assist — name, email, address, phone. This significantly reduces burden on users who rely on autofill, including users with motor impairments and cognitive disabilities. - For groups of related fields (date parts, name parts, checkbox groups), always use
<fieldset>and<legend>rather than a heading + div wrapper.
Focus
Every field must have a programmatically associated label — not just a visible text element nearby. There are two valid ways to achieve this:
| Method | How | When to use |
|---|---|---|
HTML <label> with for |
Set for="field-id" on the <label> and a matching id on the field. |
Native inputs. Preferred — clicking the label focuses the field automatically. |
aria-labelledby |
Set aria-labelledby="label-id" on the field, pointing to the ID of the label element. |
Custom components or cases where a <label> element can't be used. |
ARIA
| Role or attribute | When to use | Example |
|---|---|---|
| ||
| ||
| ||
| ||
| ||
|
How to apply it
A red asterisk alone is not sufficient — colour cannot be the only way to convey required status. Always pair the asterisk with a text explanation: either a form-level note ("Fields marked with * are required") or the word "required" included in the label itself. The asterisk should carry aria-hidden="true" so screen readers don't read it as a symbol — the aria-required attribute on the field communicates the same information programmatically.
Helper text and error messages
Helper text and error messages must be programmatically associated with the field using aria-describedby. Visual proximity alone is not enough — screen readers need an explicit link.
When a field has both helper text and an error message, aria-describedby should point to the currently visible one. On error, replace the helper text element's content with the error message (or swap which element is visible) and update aria-describedby accordingly. Adding role="alert" to the error element causes screen readers to announce it immediately without waiting for the user to focus the field.
Required fields
Use aria-required="true" on the field element for required fields. This is separate from the visual required indicator — both are needed. The HTML required attribute also works for native inputs and implicitly sets aria-required, but be aware it triggers browser-native validation UI which may conflict with custom error handling.
Grouped fields
When multiple related fields form a logical unit — a date split across day/month/year fields, a name split across first/last, a group of checkboxes — wrap them in a <fieldset> with a <legend> that names the group. Screen readers announce the legend when focus enters any field in the group, giving users the context they need.
Don't use a plain heading or a <div> for this — <fieldset> + <legend> is the correct semantic structure and the only one that reliably works across screen readers.
Watch out
Don't set aria-invalid="true" on page load before the user has interacted with the field. Screen readers announce the invalid state when focus arrives — marking every field as invalid on load creates a disorienting experience before the user has had a chance to fill anything in. Set aria-invalid on blur or after form submission.
Things to avoid
- Placeholder text as a label substitute — placeholder disappears on input, leaving users with no reminder of what the field expects. Always use a persistent visible label.
- Labels that aren't programmatically associated — a
<div>styled to look like a label isn't a label. Screen readers won't connect it to the field. Use<label for>oraria-labelledby. - Error messages not linked to the field — an error message positioned visually below a field isn't automatically read when the field receives focus. Always use
aria-describedbyto link them. - Colour alone to convey required or error state — a red asterisk or red border is not sufficient on its own. Always pair colour with text or iconography.
- Showing all fields as invalid on load — setting
aria-invalid="true"before the user has interacted is disorienting for screen reader users. Validate on blur or on submit. - Vague error messages — "This field is invalid" tells users nothing. Error messages must say what went wrong and, where possible, how to fix it.
- Using headings instead of
<fieldset>for field groups — headings don't create the programmatic group relationship that screen readers use. Use<fieldset>+<legend>for related fields.