Semantic form structure, <fieldset> grouping, required-field conventions, and the error-summary pattern. The foundation chapters 13–16 build on — in Vanilla and React.
WCAG: 1.3.1 · 3.3.1 · 3.3.2 · 3.3.3 · 4.1.2
Level: Intermediate
Icons: lucide-react (AlertCircle)
1
What we are building
A form that is keyboard-first, screen-reader-first, and that handles errors with real focus management.
What we are building
This chapter covers the form-level patterns: the <form> element itself, grouping with <fieldset> + <legend>, marking required fields, and the error-summary pattern used by GOV.UK, GitHub and others. Chapters 13–16 go into each control (input, select, checkbox/radio, error message) in depth.
2
The 4 real-world variants
Switch between the inaccessible version and the accessible fix.
The 4 real-world variants
Variant A
Semantic form
Preferred
Live example
Press Enterfrom any input to submit. Browsers prefill name & email from the autocomplete hints.
Real <form> gives you Enter-to-submit, native validation, and a submit event. Autocomplete attributes let browsers prefill.
Variant B
Fieldset grouping
For groups
Live example
SR with fieldset: “Shipping speed, group, Standard, 1 of 3”. Without fieldset: just “Standard” — no context.
HTML · Accessible
<!-- fieldset + legend: SR announces group name on EVERY option -->
<fieldset>
<legend>Shipping speed</legend>
<label><input type="radio" name="speed" value="std"> Standard (5-7 days)</label>
<label><input type="radio" name="speed" value="exp"> Express (2 days)</label>
<label><input type="radio" name="speed" value="ovn"> Overnight</label>
</fieldset>
Required for radio and checkbox groups. SR announces the legend before every option so users know what group they are in.
Variant C
Required / optional
Preferred
Live example
Visual “*” is aria-hidden — the browser announces “required” via the required attribute.
HTML · Accessible
<!-- Native required + visible asterisk + legend explaining it -->
<p><span aria-hidden="true">*</span> indicates a required field</p>
<label for="email">
Email <span class="req" aria-hidden="true">*</span>
</label>
<input id="email" type="email" required autocomplete="email">
<!-- OR, when most fields are required, mark the exceptions -->
<label for="company">
Company <span class="opt">(optional)</span>
</label>
<input id="company" type="text" autocomplete="organization">
Use native `required` + visible asterisk + a legend line. aria-required is redundant when `required` is set.
Variant D
Multi-field validation (focus first)
Scales to long forms
Live example
Submit empty — focus jumps to the first invalid field. SR announces the field’s error on focus.
HTML · Accessible
<!-- Error summary with role="alert", focus moved to it, links to each field -->
<form noValidate>
<div
id="err-summary"
role="alert"
tabindex="-1"
aria-labelledby="err-summary-title"
hidden
>
<strong id="err-summary-title">There are 2 problems with your submission</strong>
<ul>
<li><a href="#f-email">Email is required.</a></li>
<li><a href="#f-password">Password must be at least 8 characters.</a></li>
</ul>
</div>
<label for="f-email">Email</label>
<input id="f-email" type="email" required
aria-invalid="true"
aria-describedby="f-email-err">
<span id="f-email-err">Email is required.</span>
<button type="submit">Sign up</button>
</form>
<!-- JS: on invalid submit, show summary, move focus to it,
set aria-invalid on failing fields -->
On invalid submit, focus the first invalid field. SR reads its error on focus (via aria-describedby). Short forms (3-5 fields) can alternatively use the GOV.UK top-summary pattern.
3
Flagship implementation
A reusable AccessibleForm with validation + error summary wired up.