Skip to content

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
Frameworks
Live example
<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
Time zones
Live example
<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>

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.

Dietary needs
Select at least one
Dietary needs
Live example
<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>
  • It’s checkboxes — no role="listbox" / option re-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 controlaria-labelledby points 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 (no aria-roledescription override — that would suppress the state cue).
  • The live value reaches assistive tech through the behaviorenhanceMultiSelect writes 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-invalid and the aria-describedby error link on both the <fieldset> and the summary (the blocked submit moves focus to the summary, so it must carry them), the message is a role="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-expanded is the native <summary>’s — not managed by JS.
  • Forced colors — checked boxes keep the Highlight fill 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.