Skip to main content

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.

OverviewStyleAccessibility

Description

Anatomy

PartRequired?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.

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

Field sizes

Input, Select, Search and Textarea share one size scale, so a field lines up with a button of the same size. Heights are fixed for Input, Select and Search; a Textarea grows with its content and is padded instead.

Size Height Padding (H) Gap Font size Textarea padding (V)
Extra small 24px 8px 4px 12px 4px
Small 32px 12px 8px 14px 8px
Medium (default) 40px 16px 8px 16px 10px
Large 48px 20px 12px 16px 12px
Extra large 56px 24px 12px 20px 16px

Component tokens

The field tokens are framework-neutral CSS variables, so any framework (including React Aria components) can use them without Web Awesome. Colours follow Light and Dark mode and the product theme. Every token aliases a Semantic token, never a Primitive.

Token What it sets
--orbit-form-control-outlined-background, -filled-outlined-background Field fill for each appearance.
--orbit-form-control-border Field outline. Uses the strong border colour, so it meets 3:1 against the surface in Light and Dark.
--orbit-form-control-text, -placeholder, -icon Entered value, placeholder and icon colours. The placeholder uses quiet text so it meets 4.5:1.
--orbit-form-control-label-color, -label-font-weight, -hint-color, -hint-font-size Label and hint text.
--orbit-form-control-state-hover Translucent overlay drawn over the fill on hover.
--orbit-form-control-invalid-border, -invalid-message Outline and message colour when the field is invalid.
--orbit-form-control-disabled-background, -border, -text Disabled colours.
`--orbit-form-control-size-{xs s
`--orbit-form-control-textarea-size-{xs s
--orbit-form-control-select-multiple-padding-inline-start Left padding of a multi-select, smaller because tag chips are boxes.
--orbit-form-control-radius, -radius-pill Corner radius. Pill is used by search fields.
--orbit-form-control-border-width, `-focus-ring-{width offset
--orbit-form-control-transition-duration, -transition-easing, -font-family, -value-font-weight Motion and type.

States are Default, Hover, Invalid and Disabled. Focus is shown with the focus ring, not a separate variant. | Required indicator | Asterisk inline with label, --op-space-4 gap, colour --op-color-status-error |

Tokens

PartTokenValue
Label text --op-color-text-primary --op-color-neutral-10
Required indicator --op-color-status-error --op-color-red-50
Helper text --op-color-text-muted --op-color-neutral-30
Error message text --op-color-status-error --op-color-red-50
Error icon --op-color-status-error --op-color-red-50
Disabled opacity — 0.5

Engineering notes

  • The Form Control wrapper is responsible for the aria-describedby relationship — the field component itself should expose an id, 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-describedby links — toggle visibility at the element level or swap the aria-describedby value. Avoid display: none on elements still referenced by aria-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 autocomplete attribute 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 attributeWhen to useExample
aria-required="true" Applied to the field element. All required fields. Pair with a visible required indicator. <input aria-required="true" aria-describedby="field-hint">
aria-invalid="true" Applied to the field element. When the field has a validation error. Set after user interaction, not on load. <input aria-invalid="true" aria-describedby="field-error">
aria-describedby Applied to the field element. Points to the ID of the helper text or error message element. <input aria-describedby="field-error"><span id="field-error">Enter a valid email address.</span>
aria-labelledby Applied to the field element. When a native <label for> can't be used. Points to the label element's ID. <input aria-labelledby="field-label"><span id="field-label">Email address</span>
role="alert" Applied to the error message element. Causes the error to be announced immediately when it appears, without waiting for focus. <span id="field-error" role="alert">Enter a valid email address.</span>
aria-hidden="true" Applied to the required asterisk (*). Hides the symbol from screen readers — aria-required on the field already communicates this. <label>Email <span aria-hidden="true">*</span></label>

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> or aria-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-describedby to 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.

Was this page helpful?

Updated 2 October 2026