Checkbox, Radio and Switch

The three controls where the border is not describing the control. It is the control.

Try it

An email goes out the day before.

Space toggles either; Tab moves between them. The switch takes effect the moment it is flipped, and its description says what changed — the label names the setting and never the state.

Choosing a control

A checkbox is any number of a set — none, some, all — or one yes-or-no that applies when the form is saved. A radio is exactly one of a short set, and it starts with one chosen: a group with nothing selected cannot be put back to nothing, so it is not a choice the reader can undo. Two is the fewest radios that make sense; past six or so, the list is a Select.

A switch is one setting that takes effect the moment it is flipped. If the change applies after a Save, it is a checkbox, whatever it looks like. Label a switch for what happens when it is on — Send reminders, Allow online booking — so the off state can be inferred; Don’t send reminders turns the control into a double negative.

Anatomy

A 20px box, square for the checkbox and round for the radio, and a 40 by 20 track with a 16px knob for the switch. The label is body text, and the control is centred on its first line, so a label that wraps keeps the box at the top rather than floating beside the middle of a paragraph. The description sits under the label in the secondary colour. One size, on purpose: a field’s size changes its box and never its text, and these controls are their box.

A text input has a label, a placeholder, a height and a padded box — the border is one signal among several. An unchecked checkbox has none of that. Remove its border and there is nothing left to see, which is why these are the components border/strong exists for, and why it is the same primitive in both themes: it is the only ramp value clearing 3:1 against every surface in light and dark alike.

The box is inset — filled with surface/sunken, 1.17:1 below the card in light and the canvas colour in dark, where the ladder ends — so its border and its selected dot are measured against that fill, not only against the card behind it. The border reads 3.043:1 in light and 5.45AA in dark there, the dot 7.73AAA in dark. A fill that only looks right against the card is the hardest kind of regression to catch by eye, which is why the suite measures the box on its own.

A checkbox fills when checked; a radio keeps its fill and gains a ring and a dot. Keeping the two treatments apart is what makes them distinguishable at a glance in a form that contains both. Checked and disabled states are exempt from the border rule: a brand fill supplies the contrast, and WCAG 2.1 excludes inactive components.

Checkbox

They will get an email the day before.
Unavailable for this service.
Required by the clinic.

A disabled option keeps its label and says why in the description, so the reader learns what they cannot change and what would change it. The label names the option in every state; the reason lives underneath.

Mixed state

A parent whose children are a mix reads as aria-checked="mixed", not as checked. Toggle the children and watch the parent.

Radio

A radio on its own means nothing — its state exists only relative to the others sharing its name. Group them in a fieldset with a legend so the question is announced along with the answers, and the legend is the question, not a heading over it.

Appointment type
At the clinic.
A link is sent an hour before.
Not offered for this service.

Switch

A checkbox underneath, with role="switch". The role is the whole difference: a screen reader says “on” and “off” rather than “checked” and “not checked”, which is what a switch means.

They get an email the day before.
Not available on this plan.
Required by the clinic.

The track is border/strong off and border/success on, and the knob is surface/raised — a raised surface sitting on the track. All four pairs, knob against track in each state and mode, are in the margin; the tightest is the off state in light, 3.57:1.

The drawn switch had no visible boundary, in either half.

A mint track with a white knob measures 1.48:1 knob-to-track, and 1.48:1 track-to-card. Neither the control nor its state could be made out — it reads as a pale smudge on white. It uses the tokens that exist to clear 3:1 instead:border/strong off, border/success on, and the knob on surface/raised, which is what it is.

The knob’s position is the state signal that survives when colour does not, which is why reduced motion removes the slide but never the travel.

Accessibility

The input is wrapped in its label, so neither control can ship unlabelled by forgetting to wire an id, and the text is part of the click target. That is also what makes the target large enough: the box alone is 20px, under the 24px WCAG asks of a target, and the label beside it is what the finger lands on. Never split the two, and never leave a gap between them.

The description sits outside the label and is attached with aria-describedby. Inside it, a screen reader reads it as part of the control’s name — “Send a reminder They will get an email the day before” becomes the name rather than a name and a description. A test caught this; it was written the wrong way first.

Focus adds a ring outside the box. The border keeps its colour and its width, so a checked box stays visibly checked while focused and nothing reflows. Hover turns the border to the accent, and only the border: a state change carried by the fill would be mistaken for checked.

Arrow keys move selection within a radio group and space toggles a checkbox or a switch, because all three are real inputs underneath rather than styled divs. Under reduced motion the switch’s knob still moves; it stops sliding.

Props

Checkbox props
PropTypeDefault
indeterminatebooleanfalse
descriptionReactNode
Radio props
PropTypeDefault
descriptionReactNode
Switch props
PropTypeDefault
descriptionReactNode

The label is the children. All remaining input attributes are passed through — name, value, checked, disabled, onChange.