Form controls — choice without ambiguity
Checkbox, Radio, Switch, and Select solve different selection jobs. Mixing patterns breaks UX more than an “ugly” color.
When to use which
| Control | Job |
|---|---|
| Checkbox | Independent options; multiple allowed; “I agree to terms” |
| Radio | Exactly one option from a group (2–5 visible options) |
| Switch | Instant on/off for a setting (state, not “submit the form”) |
| Select | One (or multi) choice from a long list |
Switch ≠ Checkbox: a switch usually applies the change immediately; a checkbox is more often part of a form with Submit.
Checkbox
<label className="control">
<input type="checkbox" checked={checked} onChange={onChange} />
<span>Receive news</span>
</label>
States: unchecked, checked, indeterminate (for “select all” in a tree), disabled, error.
Indeterminate is set only via a DOM property (input.indeterminate = true), not an HTML attribute — handle it in React with a ref/effect.
Custom visuals are fine, but keep a native input (visually hidden) for a11y, or fully recreate keyboard and ARIA (role="checkbox", aria-checked). The native path is more reliable.
Radio group
<fieldset>
<legend>Plan</legend>
<label><input type="radio" name="plan" value="free" /> Free</label>
<label><input type="radio" name="plan" value="pro" /> Pro</label>
</fieldset>
- One
namefor the group. fieldset+legendprovide an accessible group name.- Up/down arrows move between radios in a native group.
Switch
<button
type="button"
role="switch"
aria-checked={on}
onClick={() => setOn((v) => !v)}
>
<span className="switch__thumb" />
<span className="switch__label">Dark theme</span>
</button>
Or a native checkbox styled as a switch — the key is agreeing on semantics and how state is announced. The label must be clickable and unambiguous (not “Yes/No” without context).
Select
Native <select> is the best start: mobile OS pickers, keyboard, a11y.
<label htmlFor="country">Country</label>
<select id="country" name="country" defaultValue="">
<option value="" disabled>Select…</option>
<option value="de">Germany</option>
<option value="fr">France</option>
</select>
A custom dropdown (listbox) is needed for richer UI (search, avatars in options). Then you must have: role="combobox" | listbox", arrow-key control, Escape, typeahead, correct aria-activedescendant or roving tabindex. That is a separate complex primitive — do not style a “div menu” without keyboard support.
Errors and forms
Link controls to error text the same way as Input (aria-invalid, aria-describedby). For radio groups, attach the error to the fieldset or describe the group as a whole.
Practice
- Checkbox + indeterminate “Select all.”
- Radio group in a fieldset.
- Switch with
aria-checked. - Native Select + a story with an error state.
- Document DS rules for “switch vs checkbox.”