Radio
One choice from a short list, with every option visible at once. Past about seven options, use a Select instead.
Which one
| Control | Answers | Takes effect |
|---|---|---|
| Checkbox | Any number, including none | On Save |
| Radio | Exactly one, all options visible | On Save |
| Select | Exactly one, from a longer list | On Save |
| Switch | On or off | Immediately |
The group
Options sharing a name are what make one choice. That shared name is
also why a radio group takes a single Tab stop, with the arrow keys moving between options.
<fieldset class="sr-radio-group">
<legend class="sr-radio-group__legend">Priority</legend>
<div class="sr-radio-group__options">
<div class="sr-radio">
<input class="sr-radio__input" type="radio" id="p1" name="pri" checked>
<label class="sr-radio__label" for="p1">Routine</label>
</div>
</div>
</fieldset>
States
The ring is round and the checkbox is square. That shape difference is the only cue telling a reader one is single-select — so it is never restyled.
<!-- Rest, hover and focus come from the stylesheet. `name` is what binds
the options into one choice, so it is present in every state. -->
<div class="sr-radio">
<input class="sr-radio__input" type="radio" id="r1" name="s">
<label class="sr-radio__label" for="r1">Unselected</label>
</div>
<div class="sr-radio">
<input class="sr-radio__input" type="radio" id="r2" name="s" checked>
<label class="sr-radio__label" for="r2">Selected</label>
</div>
<div class="sr-radio sr-radio--error">
<input class="sr-radio__input" type="radio" id="r3" name="s">
<label class="sr-radio__label" for="r3">Error</label>
</div>
<div class="sr-radio">
<input class="sr-radio__input" type="radio" id="r4" name="s" disabled>
<label class="sr-radio__label" for="r4">Disabled</label>
</div>
Type: Card Radio
A bordered box so each option can carry a description. Selection is the border turning navy, and the ring keeps its dot — so selection is never the border colour alone.
<div class="sr-radio sr-radio--card sr-radio--card-outline">
<input class="sr-radio__input" type="radio" id="pw1" name="pathway">
<label class="sr-radio__label" for="pw1">
<span class="sr-radio__title">Two-week wait</span>
<span class="sr-radio__description">Seen within 14 days of referral</span>
</label>
</div>
Type: Card
The same box, with selection filling the whole card. Label, description and dot all invert together, so the card reads as one selected object rather than a box with a highlight.
<div class="sr-radio sr-radio--card sr-radio--card-filled">
<input class="sr-radio__input" type="radio" id="cf1" name="pathway" checked>
<label class="sr-radio__label" for="cf1">
<span class="sr-radio__title">Two-week wait</span>
<span class="sr-radio__description">Seen within 14 days of referral</span>
</label>
</div>
Type: Card Icon
A 24px icon replaces the ring where the option has a recognisable mark. The icon is decorative — the label still carries the name, because an icon on its own does not identify a record type to someone who has not seen it before.
<!-- The icon renders AFTER the label in the DOM. An element between the
input and the label breaks the `:checked +` adjacency the styling needs. -->
<div class="sr-radio sr-radio--card sr-radio--card-icon">
<input class="sr-radio__input" type="radio" id="ci1" name="kind" checked>
<label class="sr-radio__label" for="ci1">
<span class="sr-radio__title">Diagnosis</span>
<span class="sr-radio__description">Coded condition on the problem list</span>
</label>
<span class="sr-radio__icon" aria-hidden="true"><!-- Icon: clinical/result --></span>
</div>
Do not mix types within one group. A group of cards beside a group of simple radios is fine; a group that is half each is not.
Horizontal
Only for two or three short options. It wraps rather than overflows, and below 480px it should be vertical.
Error
One message for the whole group. Note there is no way back to "nothing selected" once an option is chosen — where "no answer" is meaningful, give it an option of its own.
A single choice from a short list, with every option visible at once.
When to use
- Exactly one answer, from two to about seven options, where seeing all the options is part of the decision.
- Where the options need comparing against one another, priority, pathway, reason for referral.
- Where an option needs a line of explanation. That is what the card types are for.
When not to use
- More than about seven options. Use a Select. Past seven, a radio group is a wall rather than a comparison.
- Any number of answers, including none. Use a Checkbox group.
- A yes/no that takes effect immediately. Use a Switch.
- Two named options that switch a view. Use a segmented control, it reads as a control, not as a question waiting to be answered.
- Where the user must be able to undo their answer back to nothing. A radio group cannot be un-answered once a choice is made; if "no answer" is meaningful, make it an explicit option ("Not known") rather than relying on the empty state.
Types
| Type | Left of the label | Selection shown by | Use for |
|---|---|---|---|
Simple | 20px ring | The ring fills | The default. Any ordinary form question |
Card Radio | 20px ring | The card border turns navy | Options that each need a description |
Card | 20px ring | The whole card fills navy | The same, where the choice is the main thing on the screen |
Card Icon | 24px icon | The whole card fills navy | Options with a recognisable icon, a pathway, a record type |
Do not mix types within one group. A group of cards next to a group of simple radios on the same screen is fine; a group that is half each is not.
How it works
- 20px ring, 28px label offset. The same measurements as Checkbox, so a form mixing the two lines up. The ring is round and the checkbox is square, that shape difference is the only cue that one is single-select, so never restyle it.
- Group anatomy.
fieldset→ legend → hint → error → options. Options share aname; that shared name is what makes them one choice. - Vertical by default. Horizontal is only for two or three short options, and it wraps rather than overflows.
- No default selection unless there is a genuine default. Pre-selecting the first option to avoid an empty state records an answer the user never gave.
- Card types carry a description. One line, sentence case, explaining the consequence of the choice, not a restatement of the label.
- Selected cards invert. On
CardandCard Iconthe label, description and dot all go white together, so the card reads as one selected object rather than a box with a highlight. - Error sits on the group. One message above the options with a red rule down the left, never a message per option.
Do & don't
| Do | Don't |
|---|---|
| Give every option a label that answers the legend's question | Rely on the legend to complete a half-written label |
| Offer "Not known" explicitly when it is a real answer | Leave the group unanswered as a way of meaning "not known" |
| Keep option order stable between visits | Re-order by recency, so the same answer moves |
| Use one type per group | Mix card and simple options in one group |
| Let a long label wrap | Truncate a label with an ellipsis |
Accessibility
- Native
<input type="radio">. Arrow-key roving focus, Space, and form participation are the browser's behaviour, not reimplemented, which is also why a radio group takes one Tab stop, not one per option. - The legend is the group's accessible name.
hideLegendhides it visually only; it stays in the markup. - The card types keep the same input and label. The icon is decorative and hidden from assistive technology, because the label already carries the name.
- Focus on a card is drawn around the whole card, because the card is the target. On a simple radio it is drawn on the ring.
- Selection is never colour alone: the ring fills and takes a dot; a selected card inverts its whole surface.
- Disabled uses the native attribute, so the option leaves the tab order.
Known gaps
- No Blazor implementation yet.
- Figma draws
Card Radio's ring at 16px and the other card types at 20px. Code uses 20px throughout, matchingSimpleand Checkbox. The Figma set should be normalised to 20px, the deviation is recorded inradio.css. Card Iconhas no guidance yet on which icons are appropriate; the Figma variant usesclinical/resultas a placeholder.
Accessibility requirements
| Requirement | WCAG SC | How Single Record meets it | Test method |
|---|---|---|---|
| Group name is announced with each option | 1.3.1, 4.1.2 | A real fieldset and legend. hideLegend hides it visually only, so the name still reaches assistive technology. | Screen reader announce |
| The group is one Tab stop | 2.1.1 | Native radio grouping by shared name: Tab enters the group, arrow keys move within it. No custom key handling, so this is exactly browser behaviour. | Keyboard only |
| Selection is not colour alone | 1.4.1 | The ring fills and gains a white dot; a selected card inverts its whole surface as well as its border. | Greyscale review |
| The card icon is not the label | 1.1.1 | The icon is aria-hidden and the visible label carries the name, so the option is identifiable without recognising the mark. | Screen reader announce |
| Focus matches the target | 2.4.7 | A 3px Cyan/700 ring on the control for simple radios, and around the whole card for card types — because on a card the card is what is clicked. | Keyboard tab |
| Error is exposed and actionable | 3.3.1, 3.3.3 | One message on the group, referenced from it, saying what to do ("Select a priority"). | Screen reader, submit empty |
| Required state is more than an asterisk | 3.3.2 | The asterisk is decorative; the group carries aria-required. | Screen reader announce |
| Target size | 2.5.8 | The 20px ring relies on the spacing exception: 8px between options puts centres 28px apart. Card types are 56px tall and pass outright. | Measure spacing |
| Text resizes to 200% | 1.4.4 | Labels wrap under themselves rather than under the ring, and cards size from their content. | Browser zoom to 200% |