Skip to the content.

Accessible forms come down to labels, grouping, and error wiring

Accessible forms come down to labels, grouping, and error wiring

Form accessibility sounds like it should require deep ARIA expertise. It mostly does not. The overwhelming majority of it comes down to three unglamorous things: every input has a label that is programmatically tied to it, related fields are grouped so their shared context is announced, and validation errors are wired so a screen reader actually says them out loud. Get those three right and the form works for a keyboard user, a screen-reader user, and a mouse user alike — with almost no ARIA at all. Reach for exotic attributes only after these are solid, not instead of them.

The three wires of an accessible field: label-for-id, group, and error-describedby A label linked to an input by id, a fieldset grouping related inputs, and an error message linked to the input by aria-describedby so it is announced. label input#email for=id error text describedby fieldset + legend card numberexpirycvc
Three wires: a label to the input's id, related inputs inside a fieldset, and the error joined to the input by aria-describedby so it is read.

Wire one: a label tied to its input

A placeholder is not a label — it disappears on typing and many screen readers do not announce it. Use a real <label> whose for matches the input’s id, so clicking the label focuses the input and the reader announces the label when the input gets focus:

<label for="email">Email address</label>
<input id="email" type="email" name="email" autocomplete="email">

That single for/id pairing is the highest-value line in form accessibility, and the one most often skipped in favour of a placeholder.

A set of radio buttons, or the fields of an address, share context that a single label cannot carry. <fieldset> with a <legend> provides it, so the reader announces “Payment method, Credit card” rather than a bare “Credit card”:

<fieldset>
  <legend>Payment method</legend>
  <label><input type="radio" name="pay" value="card"> Credit card</label>
  <label><input type="radio" name="pay" value="paypal"> PayPal</label>
</fieldset>

Wire three: announce the error

An error the eye can see but the screen reader cannot say is not an accessible error. Tie the message to the input with aria-describedby, mark the input aria-invalid, and put the message in a live region so it is announced the moment it appears:

<input id="email" aria-invalid="true" aria-describedby="email-err">
<p id="email-err" role="alert">Enter a valid email address.</p>

aria-describedby makes the reader append the error when the input is focused; role="alert" makes it announce immediately when it renders. Together they mean a non-sighted user learns which field failed and why, at the moment it matters.

These three wires — label-for-id, fieldset grouping, and error-describedby — are 90% of form accessibility, and none of them is exotic. They are the difference between a form anyone can complete and one that silently strands a screen-reader user at a field they cannot identify. The form-field-molecule exercise builds exactly this wiring into a reusable field so you get it right once and reuse it everywhere, which is how accessible forms actually scale across an app.