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.
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.
Wire two: group related fields
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.