Fieldsets, legends & grouping
Use fieldset and legend to give related controls a shared accessible name — and learn when grouping helps, when it hurts, and the ARIA fallback.
In one line: <fieldset> and <legend> give a group of controls a shared accessible name — without them, a screen-reader user navigates a list of labeled inputs with no sense of how they're organized.
What it is
A <fieldset> wraps related form controls, and its <legend> child becomes the accessible name of that group. Screen readers announce the legend when focus enters the fieldset — so a user landing on the "Card number" field inside a fieldset with a "Payment details" legend hears "Payment details, Card number, edit text" rather than just "Card number, edit text."
The pattern solves a real problem: labels describe individual controls, but they can't communicate why controls belong together. Grouping carries the context that individual labels can't.
Why it matters
WCAG success criteria 1.3.1 (Info and Relationships) and 3.3.2 (Labels or Instructions) both require that the purpose of grouped controls is conveyed programmatically, not just visually. A form that groups billing fields under an <h3> instead of a <fieldset> and <legend> passes a visual inspection and fails an assistive-technology audit.
Grouping is also a cognitive-load tool: long forms feel shorter when partitioned into named sections, and users understand which questions belong together before they start answering.
See it
Tweak it3
The same two-group form, rendered three ways. Switch grouping mode and read what a screen reader announces when focus lands on the first field of the second group — visual headings and semantic legends look identical on screen but differ entirely in the accessibility tree.
How it works
The grouping strategies
Native <fieldset> + <legend> — the default choice. The <legend> must be the first child of the <fieldset>; placing it after an input is invalid and breaks announcement order.
<form>
<fieldset>
<legend>Personal information</legend>
<label for="first-name">First name</label>
<input id="first-name" name="first_name" autocomplete="given-name" />
<label for="last-name">Last name</label>
<input id="last-name" name="last_name" autocomplete="family-name" />
</fieldset>
</form>Radio and checkbox groups — fieldset is required. Individual labels ("Yes", "No") make no sense without the question; the <legend> provides it.
<fieldset>
<legend>How did you hear about Marrow?</legend>
<label><input type="radio" name="source" value="search" /> Search engine</label>
<label><input type="radio" name="source" value="social" /> Social media</label>
<label><input type="radio" name="source" value="friend" /> Word of mouth</label>
</fieldset>Without the fieldset, a screen-reader user lands on "Search engine, radio button, 1 of 3" with no idea what they're choosing between.
ARIA role="group" — the fallback. When a native <fieldset> can't be used (a grid/flex layout the fieldset box interferes with, or a third-party component tree), use ARIA. It's functionally equivalent in modern screen readers — but reach for it only when native HTML genuinely can't fit.
<div role="group" aria-labelledby="address-heading">
<p id="address-heading">Shipping address</p>
<label for="street">Street address</label>
<input id="street" name="street" autocomplete="street-address" />
<label for="city">City</label>
<input id="city" name="city" autocomplete="address-level2" />
</div><optgroup> inside a <select> — the underused one. For grouped options, <optgroup> is announced by screen readers and renders a visual section label, all without JavaScript.
<label for="timezone">Timezone</label>
<select id="timezone" name="timezone">
<optgroup label="Americas">
<option value="America/New_York">Eastern (UTC-5)</option>
<option value="America/Chicago">Central (UTC-6)</option>
</optgroup>
<optgroup label="Europe">
<option value="Europe/London">London (UTC+0)</option>
<option value="Europe/Paris">Paris (UTC+1)</option>
</optgroup>
</select>When to group (and when not to)
Group when controls share a topic (all contact fields), serve the same function (day/month/year of one date), are interdependent (country determines available states), or are radio/checkbox controls whose individual labels lack context.
Don't group when a control stands alone (a solo email field needs no fieldset), the grouping is purely cosmetic, or the grouping would need to nest — <fieldset> elements can't nest programmatically, so use role="group" for any second level.
Legend length and screen-reader behavior
Behavior for <legend> varies by screen reader:
- NVDA + Firefox: reads the legend on entering and exiting the group.
- JAWS: reads it with the first field only.
- VoiceOver (iOS/macOS): reads it with every field.
Design for the verbosest case. A long legend like "Billing address — must match the address on file with your card issuer" gets read before every field. Keep legends to five words or fewer, and put any explanation in hint text beneath the legend, not inside it.
Styling fieldsets without breaking semantics
The default browser border is the first thing designers want gone — that's fine, the semantics live in the HTML, not the CSS.
fieldset {
border: none;
padding: 0;
margin: 0;
}
legend {
font-weight: 600;
font-size: 1rem;
margin-bottom: 0.75rem;
padding: 0;
}A <legend> can contain a heading element to slot into the page's heading hierarchy; the reverse (a heading containing a legend) is not valid.
Build it
A small FieldGroup + Field abstraction renders real <fieldset>/<legend> markup while keeping authoring terse.
import { useId } from "react";
function FieldGroup({
legend,
hint,
children,
}: {
legend: string;
hint?: string;
children: React.ReactNode;
}) {
const hintId = useId();
return (
<fieldset>
<legend>{legend}</legend>
{hint && (
<p id={hintId} className="field-group-hint">
{hint}
</p>
)}
<div className="field-group-fields">{children}</div>
</fieldset>
);
}
function Field({
label,
id,
type = "text",
required,
autoComplete,
}: {
label: string;
id: string;
type?: string;
required?: boolean;
autoComplete?: string;
}) {
return (
<div className="field">
<label htmlFor={id}>
{label}
{required && <span aria-hidden="true"> *</span>}
</label>
<input id={id} name={id} type={type} required={required} autoComplete={autoComplete} />
</div>
);
}
export function MarrowRegistration() {
return (
<form noValidate>
<FieldGroup legend="About you">
<Field label="Full name" id="name" required autoComplete="name" />
<Field label="Email address" id="email" type="email" required autoComplete="email" />
</FieldGroup>
<FieldGroup legend="Shipping address" hint="We ship to most countries.">
<Field label="Street address" id="street" autoComplete="street-address" />
<Field label="City" id="city" autoComplete="address-level2" />
<Field label="Postal code" id="postal-code" autoComplete="postal-code" />
</FieldGroup>
<button type="submit">Create account</button>
</form>
);
}If a group strips the fieldset border in your design system, make sure the abstraction still renders a real <fieldset> in the DOM — role="group" is a fallback, not a replacement.
Make it yours
Use the controls beside the demo above to change grouping mode, mark “street address” required, and show “street address” as invalid — each change updates the example live.
Experiment in the playground- Use a
<legend>with a heading inside when the group doubles as a page section heading. - For very long forms, combine fieldset grouping with the multi-step pattern — each step becomes a
<fieldset>whose<legend>is the step title. - Test every grouping with a screen reader in browse/reading mode (not forms mode) to confirm the group name is discoverable during navigation.
Reproduce it with an LLM
Reproduce it with an LLM
You are a senior front-end engineer. Build a multi-section checkout form for the fictional brand Marrow using semantic <fieldset> and <legend> elements. It has three groups: personal information, shipping address, and payment method. Each group has a short legend (≤5 words), appropriate autocomplete attributes on every input, visible focus rings, and accessible error states using aria-invalid and aria-describedby. Use vanilla HTML and CSS; no JavaScript required for the static structure. Return only the HTML and CSS.
Pitfalls & accessibility
- Don't wrap individual controls in fieldsets — a solo input doesn't need one, and overuse turns every field into its own announcement.
- Visual-only grouping (a heading, a divider) is not semantic grouping. WCAG SC 1.3.1 requires the association to live in the code, not just on screen.
<fieldset>can't nest programmatically. You can visually nest them, but screen readers handle nested legends inconsistently — userole="group"for second-level grouping.
Related
The dropdown taxonomy
Every dropdown is really one of ten patterns — native select, listbox, combobox, multiselect, and more. Learn each ARIA contract and when to reach for it.
Usability heuristics
A short checklist of interface common sense — status, control, consistency, error recovery — you can audit any screen against in minutes.