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

ControlJob
CheckboxIndependent options; multiple allowed; “I agree to terms”
RadioExactly one option from a group (2–5 visible options)
SwitchInstant on/off for a setting (state, not “submit the form”)
SelectOne (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 name for the group.
  • fieldset + legend provide 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

  1. Checkbox + indeterminate “Select all.”
  2. Radio group in a fieldset.
  3. Switch with aria-checked.
  4. Native Select + a story with an error state.
  5. Document DS rules for “switch vs checkbox.”