The wiring nobody wants to write twice

Every accessible form control needs the same plumbing: a stable id, a label pointing at it, error text linked through `aria-describedby`, an invalid state exposed through `aria-invalid`, and hint text linked without clobbering the error link. It is not difficult, it is repetitive, and repetition is where mistakes live.

`Field` owns that wiring. It generates the id, associates the label, links hint and error text in the right order, and sets the invalid state. Inputs placed inside it inherit correct semantics without each one reimplementing them.

Placeholders are not labels

A placeholder disappears when the user types, which means the field's purpose vanishes exactly when they are filling it in. It fails for users with cognitive load, for anyone returning to a half-completed form, and its contrast is usually poor by design.

Every field needs a real, visible `<label>`. If your design genuinely cannot show one, the control still needs an accessible name — but that is a compromise, not a solution, and it is worth pushing back on the design first.

Errors must be announced, not just shown

Turning a border red and adding text below communicates nothing to a screen reader user unless the error is linked to the input and announced. `aria-describedby` pointing at the error element plus `aria-invalid` on the input is the mechanism.

Timing matters too. Validating on every keystroke announces errors while the user is mid-word, which is hostile. Validate on blur, or on submit, and if you must validate live then debounce it well past typing speed.

Grouped controls need a fieldset

A set of radios is not five independent controls, it is one question with five answers. `RadioGroup` is a real `<fieldset>` with a `<legend>`, which names the group using no ARIA at all — the browser already knows what a legend means.

This is the general principle across the library: reach for the native element before reaching for ARIA. `Divider` is a real `<hr>` unless it is labelled. `FAQ` is native `<details>`. Native semantics work before hydration, survive CSS being disabled, and cannot be subtly wrong.

The specialised inputs

`Calendar` is a real `role="grid"` with the full APG keyboard map, localised through `Intl` rather than a date library — which is part of how the package keeps zero runtime dependencies. `Combobox` moves the active option with `aria-activedescendant` while DOM focus stays in the input, per the ARIA pattern.

That `aria-activedescendant` detail is the one hand-rolled comboboxes get wrong most often. Moving real DOM focus into the listbox breaks typing, so the pattern keeps focus in the input and points at the active option instead. `OTPInput`, `TagInput`, `Rating`, `Slider` and `FileUpload` each have equivalent models, which is the reason they exist as components rather than as styled inputs.

How to check your own forms

Three checks find most problems. Click every label and confirm focus moves to its control — that proves association. Tab through the form with your eyes closed and see whether you could complete it. Trigger a validation error and confirm a screen reader announces it rather than only showing it.

None of these need specialist tooling and all three catch real defects. Automated checks like axe catch what is machine-detectable; the keyboard pass catches what is not, and the gap between them is where most shipped form bugs live.

Key takeaways

  • Field owns id generation, label association, describedby wiring and invalid state, so inputs inherit correct semantics
  • Placeholders vanish when typing — every field needs a real visible label
  • Errors need aria-describedby and aria-invalid, validated on blur rather than every keystroke
  • RadioGroup is a fieldset with a legend, which names the group with no ARIA at all
  • Combobox keeps DOM focus in the input and moves the active option with aria-activedescendant
  • Test by clicking labels, tabbing the form with eyes closed, and confirming errors are announced

Conclusion

Form accessibility is a small set of rules applied consistently, and consistency is exactly what hand-wiring every input fails at. Centralising the plumbing in one component means the correct behaviour is the default rather than something each field has to remember.

Enjoyed this article?

Vivek Kumar Singh

Vivek Kumar Singh

Technical Expert · Full Stack Cloud Engineer · Tokyo, Japan