Components · Forms

Radio

Exactly one from a set shown in full, with nothing chosen until somebody chooses it.

Stable · v1.0 shadcn/ui WCAG 2.2 AA

Overview

A radio group asks a question whose answers are mutually exclusive and shows all of them, which is what separates it from a Select: the options are part of the question. Two things about it are easy to get wrong, and both put an answer in the record that nobody gave.

Which of the three to reach for — how many may be chosen, and when the choice takes effect — is decided on Selection controls.

Nothing is selected by default

The rule

A clinical radio group starts with nothing selected. A pre-selected option is an answer nobody gave, and in the record it is indistinguishable from one somebody did.

Arrowing through a radio group answers it

A radio group is traversed with the arrow keys, and on the web arrow keys in a radio group move focus and select. Moving from Yes to Unknown selects No on the way. This is native behaviour, endorsed by ARIA's authoring practices, and this system keeps it — the alternative is a control that behaves unlike every other radio group the clinician has ever used.

The rule

A change event from a radio group is a selection, not an answer. Three keystrokes produce three changes and one intention. Nothing may be recorded, submitted, acknowledged or billed on the strength of one.

What the component enforces, and what it only asks for

Almost none of the safety argument on this page is held by the component, and the reason is worth stating plainly: a radio is controlled by whoever passes checked.

RuleHeld by How
A group states its shared questionThe component RadioGroup is a real fieldset with a legend.
Asserted: RadioGroup: the shared question is a real legend
One answer, one tab stop, arrows insideThe component RadioGroup gives its radios a shared name, which is what makes the platform treat them as one question. It did not: a caller who left name off got two answers checked at once to a yes/no question and a tab stop per option. An explicit name still wins. Driven in a browser.
Asserted: Radio: a group whose radios carry no name still allows only one answer
The whole label row is the targetThe component From the shared base file, at 44 px.
Asserted: Radio: the label row is the target, not the control
An unavailable option says why, and stays reachableThe component disabled is not a prop, as on Button. unavailable={{ reason }} keeps the control focusable, describes it by the reason, and ignores Space and clicks. It accepted a bare native disabled: off, unreachable by keyboard, and silent about why.
Asserted: Checkbox, radio and switch cannot be switched off without a reason
Nothing is selected by defaultYou checked and defaultChecked pass straight through. A pre-selected clinical radio is an answer nobody gave, and this component will render one without complaint.
A change event is not treated as an answerYou Arrow keys select as they traverse, so moving from Yes to Unknown fires a change for No. If your handler writes to the record, it has just recorded that the patient is not pregnant.
“Unknown” is one of the optionsYou The options are whatever you render. A group with nowhere to put a genuine non-answer produces guesses.
Why this is written down

A rule stated on a component page reads as a rule the component keeps. 2 of these 6 are; 3 are obligations this page was quietly handing to the reader. Three of the six are yours, and they are the three that decide what ends up in the record. The interaction gate asserts the traversal behaviour precisely because the component cannot protect you from it.

Do's and don'ts

Do

Yes · No · Unknown

recorded on submit

Two change events passed through on the way. Neither reached the record, because the record is written when the form is.

Don't

Yes · No · Unknown

saved on change · 14:22 · 14:22 · 14:22

Three entries, one intention, and the middle one says the patient is not pregnant. Nobody typed that.

Do

Pregnant?

○ Yes   ○ No   ○ Unknown

Nothing selected. The question is visibly unanswered until somebody answers it.

Don't

Pregnant?

○ Yes   ● No   ○ Unknown

“No” pre-selected. In the record this is indistinguishable from a clinician having asked and been told no, and nobody asked.

Do

Sampling interval

● 0/1 h algorithm

○ 0/3 h algorithm

Both options visible. The reader chooses between things they can see.

Don't

Sampling interval: 0/1 h algorithm ▾

A two-option choice hidden in a select. The alternative is invisible until opened, and a reader who never opens it never learns there was a choice.

Accessibility

Outcomes of use

Clinical safety notes

Risk controls this page specifies

Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.

Implementation

$ npx shadcn@latest add https://md.notjustany.tech/r/radio.json
<RadioGroup legend="Pregnant?">
  // no defaultChecked: an unanswered question stays unanswered
  // the group names its radios, so they are one question without a `name` here
  <Radio label="Yes" />
  <Radio label="No" />
  <Radio label="Unknown" />
</RadioGroup>
NotJustAnyMed.Tech Design System · Selection controls · v1.0 · draft for review
Covers Checkbox, Radio and Switch. Reference applications named in this system are fictional.