Group related checkboxes with a fieldset and legend
Canonical URL: https://devexamples.com/html/group-related-checkboxes-with-fieldset-and-legend/
The problem
A form asks a person to pick any number of notification channels. Each box needs its own name, and the question those boxes answer together needs a name too, otherwise a screen reader announces four unrelated checkboxes. The group may be disabled as a unit, and the submitted payload must keep every chosen value. Current browsers and their bundled screen readers are assumed.
Short answer
Wrap the boxes in a fieldset whose first child is a legend stating the question, give each checkbox its own label with for and id, share one name across the boxes so the server receives a list, and use radio buttons in the same structure when the choices are mutually exclusive.
Four checkboxes under one heading can be announced as four unrelated controls, because the heading is only a heading. The group needs a name that arrives with the first control, and the boxes inside it still need names of their own.
<fieldset class="channels">
<legend>How should we reach you?</legend>
<div class="choice">
<input type="checkbox" id="ch-digest" name="channels" value="digest">
<label for="ch-digest">Monthly digest</label>
</div>
<div class="choice">
<input type="checkbox" id="ch-security" name="channels" value="security">
<label for="ch-security">Security notices</label>
</div>
<div class="choice">
<input type="checkbox" id="ch-product" name="channels" value="product">
<label for="ch-product">Product updates</label>
<p class="hint">Roughly one message a month, opt out any time.</p>
</div>
</fieldset><fieldset class="cadence">
<legend>How often?</legend>
<div class="choice">
<input type="radio" id="cad-week" name="cadence" value="weekly" checked>
<label for="cad-week">Weekly</label>
</div>
<div class="choice">
<input type="radio" id="cad-never" name="cadence" value="never">
<label for="cad-never">Never</label>
</div>
</fieldset>.channels,
.cadence {
border: 1px solid var(--de-border, #c7ccd4);
border-radius: 0.5rem;
padding: 1rem;
min-inline-size: 0;
}
legend {
padding-inline: 0.5rem;
font-weight: 600;
}
.choice {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem;
align-items: start;
margin-block-end: 0.75rem;
}
.choice label {
grid-column: 2;
}
.choice .hint {
grid-column: 2;
margin-block: 0;
}Explanation
The fieldset element is the native group of controls, and its implicit group role takes its accessible name from the legend that must be its first child. Screen readers use that name when focus enters the group, so the reader hears the question before the first checkbox: the group name, then the control’s own label, its checked state and its position in the set. A visually similar heading above a div produces no such announcement, because the relationship between heading and controls is not exposed; the legend is not styled text, it is the group’s name, and the fieldset and legend have to be the same element pair for it to exist at all. Note what the legend does not do: it never becomes the name of a control inside the group, so each checkbox still needs its own label with matching for and id, exactly as in a standalone field.
The shared name does two separate jobs that are easy to confuse. For submission it produces one entry per checked box, so the payload looks like channels=digest&channels=security, and an unchecked box contributes nothing, which is why “no channels selected” and “this section was not rendered” are indistinguishable on the server unless you handle the missing key deliberately. For behaviour, name is what defines a radio group: the two radios above are mutually exclusive because they share a name in the same tree, not because the fieldset wraps them, and deleting the fieldset would break the announcement while leaving the exclusivity intact. That difference is also why an either-or question must not be built from checkboxes: nothing about a checkbox group constrains checkedness, so a person can select every option, and the form either accepts a state the product cannot honour or rejects it with an error no native attribute could have predicted. The inverse mistake matters too, because a set of independent toggles built from radios silently discards all but one selection.
Two boundaries shape the markup. Validation cannot be expressed at group level: required belongs to a single control, so on a checkbox it means “this box must be ticked” for consent patterns, while on one member of a radio group it means “a choice is required”, which is why a “pick at least one channel” rule needs script, aria-invalid or aria-describedby on something the reader will actually reach, and a live region for the result. Styling is the other boundary: the default fieldset is a block box with a border that legend is drawn into rather than a normal flow child, which is why it resists shrinking inside a grid track until min-inline-size: 0 releases it, and why the .choice wrappers above carry the layout instead of the fieldset. The disabled attribute on the fieldset is the one inherited state worth using: it turns off every descendant, removes their values from submission, and takes them out of the tab order in a single place, whereas readonly has no effect on checkboxes or radios at all.
Usage notes
- One fieldset may nest another when a subgroup needs its own question; each legend names only its own group.
- The legend text is the group name, so keep the label text on each box short and specific rather than repeating the legend.
- disabled on the fieldset switches off and opts every descendant out of submission, which readonly cannot do for checkboxes or radios.
Common mistakes
- Building an either-or choice from checkboxes because they look lighter, then shipping a state where two mutually exclusive boxes are checked.
- Treating the legend as the label of each control and leaving the checkboxes unnamed.
- Removing the fieldset to fix its default border and replacing it with a div plus role equals group, losing the native group semantics for no gain.
Caveats
- No HTML attribute expresses at least one of these, so a group-level rule needs script and a place to announce the result.
- A fieldset will not shrink below the width of its own legend text until you set min-inline-size: 0, which surprises people inside grid tracks.
Related examples
Editorial links first, then deterministic same-task or same-topic candidates. Tags and shared language alone never qualify a candidate.
Related
Announce field errors without moving the caretA group-level message needs its own live region rather than one per checkbox.
Same task or topic
Associate every form control with an explicit labelSolves the Author accessible form markup task
Same task or topic
Style a visible keyboard focus ring with custom propertiesShares the Form Validation topic
References
- The fieldset element - HTML Living Standard(opens in a new tab) — WHATWG. Describes the group of controls, the associated legend and the disabled behaviour.
- The legend element - HTML | MDN(opens in a new tab) — MDN Web Docs. Requires the legend as first child and documents its caption role.
- Role group - Accessible Rich Internet Applications 1.2(opens in a new tab) — W3C. The role fieldset maps to and when an authored group is appropriate instead.
Source page: https://devexamples.com/html/group-related-checkboxes-with-fieldset-and-legend/