Frontend Architecture
Accessible forms: code reading
Form accessibility is decided in the markup and the submit handler. Read each snippet the way you would in a PR review, then choose the fix a senior engineer makes first.
Practise the review loop: spot the broken label association, the live region that announces nothing, the missing focus move, and the controlled input that drops the field name — then reach for the correct fix.
Snippet 1 — the label that associates with nothing
function EmailField() {
return (
<div className="field">
<label>Email</label>
<input type="email" name="email" placeholder="you@example.com" />
</div>
);
}
A screen reader announces this field as 'edit text' with no name. What is the bug and the fix?
Snippet 2 — the error region that announces nothing
function Form() {
const [error, setError] = useState("");
return (
<form onSubmit={validate}>
<input id="zip" name="zip" aria-describedby="zip-err" />
{error && <div id="zip-err" role="alert">{error}</div>}
<button>Submit</button>
</form>
);
}
Sighted users see the error; many screen-reader users hear nothing. Why, and what is the minimal fix?
Snippet 3 — the submit handler that forgets focus
function handleSubmit(e) {
e.preventDefault();
const invalid = fields.filter((f) => !f.valid);
invalid.forEach((f) => {
f.el.setAttribute("aria-invalid", "true");
setMessage(f.id, f.error); // writes into a persistent live region
});
// ...nothing else
}
aria-invalid is set and the messages are announced, yet keyboard users say the form 'does nothing' on a failed submit. What is missing?
Snippet 4 — the controlled input that drops its name
function NameField({ value, onChange }) {
return (
<>
<span className="lbl">Full name</span>
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</>
);
}
This is a correctly controlled React input, yet it fails accessibility. What is the real problem and fix?
Every form accessibility bug is visible in the markup or the submit handler: a label must be associated by htmlFor/id or wrapping (a placeholder or sibling span is not a name); a live region must be persistent so its text change is observed; a failed submit must imperatively move focus to the first invalid field, not just set aria-invalid; and controlled-vs-uncontrolled is orthogonal to whether the input has an accessible name. Read the markup, find the broken association or the un-moved focus, then fix that before anything else.