Multi-select
Multi-selection without a custom ARIA widget: a native <details> / <summary>
disclosure wrapping a <fieldset> of native checkboxes. One name, many values,
real form submission, native keyboard — the checkboxes are the control. The
optional enhanceMultiSelect behavior
adds the live “N selected” summary, Escape / outside-click close, and required
validation.
Frameworks
Select frameworks
<div class="stack">
<div class="re-field">
<span class="re-field__label" id="fw-label">Frameworks</span>
<details class="re-multiselect" data-re-multiselect>
<summary class="re-multiselect__summary" aria-labelledby="fw-label fw-value">
<span class="re-multiselect__value" id="fw-value" data-placeholder>
Select frameworks
</span>
</summary>
<fieldset class="re-multiselect__panel">
<legend class="re-sr-only">Frameworks</legend>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="fw" value="react" /> React
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="fw" value="vue" checked /> Vue
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="fw" value="svelte" checked />
Svelte
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="fw" value="angular" /> Angular
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="fw" value="solid" /> Solid
</label>
</fieldset>
</details>
</div>
</div>
Time zones
Select time zones
<div class="stack">
<div class="re-field">
<span class="re-field__label" id="tz-label">Time zones</span>
<details class="re-multiselect" data-re-multiselect open>
<summary class="re-multiselect__summary" aria-labelledby="tz-label tz-value">
<span class="re-multiselect__value" id="tz-value" data-placeholder>
Select time zones
</span>
</summary>
<fieldset class="re-multiselect__panel">
<legend class="re-sr-only">Time zones</legend>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="tz" value="utc" checked /> UTC
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="tz" value="cet" checked /> Central
European
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="tz" value="est" /> Eastern
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="tz" value="pst" /> Pacific
</label>
</fieldset>
</details>
</div>
</div> Required
Section titled “Required”data-re-multiselect-required enforces at least one selection: an empty submit is
blocked, the <fieldset> gets aria-invalid, and the .re-validation-message
(referenced by aria-describedby) is revealed.
<form class="stack" id="prefs-form">
<div class="re-field">
<span class="re-field__label" id="diet-label">Dietary needs</span>
<details class="re-multiselect" data-re-multiselect data-re-multiselect-required>
<summary class="re-multiselect__summary" aria-labelledby="diet-label diet-value">
<span class="re-multiselect__value" id="diet-value" data-placeholder>
Select at least one
</span>
</summary>
<fieldset class="re-multiselect__panel" aria-describedby="diet-error">
<legend class="re-sr-only">Dietary needs</legend>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="diet" value="vegan" /> Vegan
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="diet" value="halal" /> Halal
</label>
<label class="re-multiselect__option">
<input type="checkbox" class="re-checkbox" name="diet" value="gluten-free" />
Gluten-free
</label>
</fieldset>
</details>
<span class="re-validation-message" id="diet-error" hidden>
Pick at least one option.
</span>
</div>
<button class="re-button" data-variant="primary" type="submit">Save</button>
</form> Accessibility
Section titled “Accessibility”- It’s checkboxes — no
role="listbox"/optionre-implementation, so native checkbox semantics, keyboard, and grouping (<fieldset>/<legend>) come for free. A closed<details>keeps its checkboxes out of the tab order natively; open puts them back in. - The summary names and values the control —
aria-labelledbypoints the summary at the field label and the value text, so collapsed it announces both “what” and “what’s chosen”, while the native<summary>keeps its disclosure role and expanded/collapsed state (noaria-roledescriptionoverride — that would suppress the state cue). - The live value reaches assistive tech through the behavior —
enhanceMultiSelectwrites the “N selected” summary text and the count into a polite live region, so toggling boxes while the panel is open is announced (the summary value is the control’s name, which screen readers don’t re-announce on change). Without the behavior the control still submits and works as a native disclosure, but the summary only ever reads the authored placeholder — be honest about that. - Required is behavior-owned — a checkbox group has no native “≥1” constraint, so
the behavior enforces it:
aria-invalidand thearia-describedbyerror link on both the<fieldset>and the summary (the blocked submit moves focus to the summary, so it must carry them), the message is arole="alert"so its reveal is announced, and the form submit is blocked. The message lives outside<details>so it stays visible while the control is collapsed. - Escape closes the panel and returns focus to the summary; an outside click
closes it too.
aria-expandedis the native<summary>’s — not managed by JS. - Forced colors — checked boxes keep the
Highlightfill from.re-checkbox; the summary’s focus ring becomes the global system outline; invalid is carried by the message text (which survives forced colors), not colour alone.
See the accessibility guide for the project-wide approach.