On this page
Let users select exactly one option from a group of mutually exclusive choices.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- When exactly one option must be selected from a group and no partial selection is valid
- When the full set of options should be visible at once, so users can compare before choosing
- When there are typically 2 to 7 options (for longer lists, consider Select Field)
When not to use
- When users can select more than one option - use Checkbox instead. Radios imply that only one selection is valid.
- When there are many options - use Select Field instead. A long radio group is difficult to scan and takes up significant vertical space.
- When there is only one option - a single radio button makes no sense semantically. Use Checkbox for a binary on/off choice.
- To show a pre-selected option the user cannot change - the disabled state is not supported. Display the value as plain text or use a read-only Summary List instead. Do not use the disabled prop.
Anatomy

- Group label
- Group hint text
- Radio control
- Item label
- Item hint text
- Error text
Basic usage
Use RadioField to group related radio buttons. The label prop becomes the group's legend, which screen readers announce before each individual option. Pass a name prop so all radios in the group share the same form field name.
Edit live code
<RadioField name="choose-climate-action-basic" label="Choose a climate action" defaultValue="reduceEm" > <Radio label="Reduce emissions" id="emissions-basic" value="reduceEm" /> <Radio label="Save energy" id="energy-basic" value="saveEn" /> <Radio label="Reduce, reuse, recycle" id="3r-basic" value="3r" /> </RadioField>
Variants and features
With hint text
Hint text can be added at the group level to clarify the question, or at the item level to explain a specific option. Add item hint text only when the label alone may be unclear. Avoid adding hints to every option as a matter of course.
Group hint text clarifies the question as a whole:
Edit live code
<RadioField name="climate-action-group-hint" label="Choose a climate action" hint="Choose the action where you can make the most difference" defaultValue="reduceEm" > <Radio label="Reduce emissions" id="emissions-group-hint" value="reduceEm" /> <Radio label="Save energy" id="energy-group-hint" value="saveEn" /> <Radio label="Reduce, reuse, recycle" id="3r-group-hint" value="3r" /> </RadioField>
Item hint text explains a single option that the label alone does not make clear:
Edit live code
<RadioField name="climate-action-item-hint" label="Choose a climate action" defaultValue="reduceEm" > <Radio label="Reduce emissions" id="emissions-item-hint" value="reduceEm" /> <Radio label="Act against forest loss" id="trees-item-hint" value="trees" hint="We plant a tree on your behalf each month" /> <Radio label="Save energy" id="energy-item-hint" value="saveEn" /> </RadioField>
Horizontal layout
Set horizontal on RadioField to lay out options side by side. Only use horizontal layout when there are 2 or 3 options, labels are short, and no item hint text is present. For most groups, the default vertical layout is easier to scan.
Edit live code
<RadioField name="paperless-billing" label="Send bills by" defaultValue="email" horizontal > <Radio label="Email" id="billing-email" value="email" /> <Radio label="Post" id="billing-post" value="post" /> </RadioField>
Optional groups
Mark the group as optional with the optional prop on RadioField. This appends "(optional)" to the group label. Required groups do not need a label suffix. Required is the default expectation.
Edit live code
<RadioField label="Preferred contact method" name="contactMethod" optional> <Radio id="email" label="Email" value="email" /> <Radio id="phone" label="Phone" value="phone" /> <Radio id="post" label="Post" value="post" /> </RadioField>
Common form features
For validation, error messages, and integration with the Form component, see the Forms pattern guide.
usage
- Radio must not be used when more than one option may be selected at a timeRadios imply mutually exclusive selection. Using them for multi-select misrepresents the input's behaviour and prevents users from selecting more than one valid option.Copy rule ID for radio.not-for-multi-select
- RadioField must not contain only one Radio optionA single radio button cannot be deselected once chosen. This traps the user in a selection with no way to undo it. Use Checkbox for a single binary choice instead.Copy rule ID for radio.not-for-single-option
- Radio must not be passed the disabled propThe prop passes through to the native input with no Nebula styling and removes the field from the tab order, making it invisible to keyboard users with no accessible indication of why.Copy rule ID for radio.no-disabled
- Radio should be replaced with plain text. For example in a SummaryList. When displaying a value the user cannot editA field that looks interactive but cannot be changed is confusing. Plain text communicates the value without implying editability.Copy rule ID for radio.non-editable-as-text
- Radio should not be pre-selected by default unless restoring a value the user previously chosePre-selecting an option increases the chance that users overlook the question and submit an unintended answer. GDS research consistently shows pre-selection causes errors, particularly on infrequently used forms. Only restore a prior selection when the value was explicitly chosen by this user.Copy rule ID for radio.no-preselection
placement
- RadioField should not use horizontal layout when there are more than 3 options, labels are long, or any option has item hint textHorizontal layout works only for compact, scannable groups. Long labels or hint text break the alignment and make the group harder to read. Vertical layout is the safe default.Copy rule ID for radio.horizontal-layout-conditions
content
- Radio group must contain options that are mutually exclusive and collectively cover the valid responsesThis is the semantic contract of a radio group. If options overlap or the set is incomplete, users may be unable to accurately represent their answer, or may select a misleading option.Copy rule ID for radio.labels-mutually-exclusive
- Radio item label should be written in sentence caseCopy rule ID for radio.label-sentence-case
- Radio group should list options in alphabetical order unless a different order more closely matches users' mental model or frequency of selectionAlphabetical order has no implied hierarchy and is easy to scan. Frequency-based ordering may be appropriate when one option is chosen by the large majority of users. "None" or "I don't know" options should appear last.Copy rule ID for radio.alphabetical-order
- Radio item label should not end with a comma, semicolon, or full stopTrailing punctuation after radio labels adds visual noise. Labels are not sentences in this context.Copy rule ID for radio.label-no-trailing-punctuation
- Radio item hint text should not be added to every option in a group as a defaultHint text on every item increases reading burden without adding value for options that are self-explanatory. Add item hint text only when a specific option needs clarification beyond its label.Copy rule ID for radio.item-hint-selective
- If the question may have no valid answer for some usersRadio group may include a "None" or "I don't know" option rather than forcing users to skip the questionWhere no option genuinely applies, forcing a choice produces inaccurate data. A clearly labelled opt-out preserves data integrity and reduces user frustration.Copy rule ID for radio.none-option
validation
- RadioField error text should follow the pattern "Select [a/the] [noun]" when no option has been selectedThis pattern is direct and tells the user exactly what action to take. Vague messages like "This field is required" do not describe what to select.Copy rule ID for radio.group-error-message-pattern
- Radio error text should not be shown at both the group level and the individual item level simultaneouslyShowing the same error twice adds noise and implies separate problems. Use RadioField's error prop to surface a single group-level error.Copy rule ID for radio.group-error-not-duplicated
interaction
- RadioField will set aria-invalid on the fieldset and data-error on child Radio inputs automatically when an error is presentCopy rule ID for radio.will-aria-invalid
composition
- RadioField will set aria-required on each child Radio input automatically, based on the optional propCopy rule ID for radio.will-aria-required
- RadioField will propagate its name prop to all child Radio inputs automatically, unless a Radio has its own name propCopy rule ID for radio.will-name-propagation
- Radio group must be wrapped in a RadioField when two or more Radio components share a common purposeRadioField renders a fieldset and legend, which is the accessible HTML pattern for grouping form controls. Without it, screen readers cannot associate individual radio buttons with their group label.Copy rule ID for radio.group-requires-radiofield
- Radio must have a unique id propThe id prop links the native input to its label element via htmlFor. Without it, the label is not programmatically associated with the input, making the radio button inaccessible to screen readers.Copy rule ID for radio.id-required
Radio
Also accepts all props from HTMLAttributes, InputHTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
id | string | — | Yes | Unique `id` for the input. Links the label and hint to it. |
label | ReactNode | — | Yes | Text shown next to the radio button. |
hint | ReactNode | — | No | Helper text shown below the label and linked to the input via `aria-describedby`. |
RadioField
Also accepts all props from FieldsetHTMLAttributes, HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | Yes | Shared `name` applied to every `Radio` in the group, unless a `Radio` sets its own. |
defaultValue | string | — | No | The `value` of the `Radio` that is selected initially (uncontrolled). |
label | ReactNode | — | Yes | Legend text that labels the group of radio buttons. |
hint | ReactNode | — | No | Helper text shown under the label. |
error | ReactNode | — | No | Error message shown below the group. Marks the group as invalid (`aria-invalid` on the fieldset) and gives its radios the error styling. Falls back to the error for this `name` from the surrounding `Form`. |
optional | boolean | false | No | Shows the field as optional rather than required, and stops its radios being marked `aria-required`. |
value | string | — | No | The `value` of the selected `Radio` (controlled). Use with `onValueChange`. |
horizontal | boolean | — | No | Lays the radio buttons out in a row instead of a column. |
onValueChange | (newValue: string | undefined) => void | — | No | Called with the `value` of the radio button the user selects. |
Styles
radio-item
Defined in Radio/Radio.tsx
display: flex;
flex-wrap: wrap;
position: relative;
padding: ${space[1]} 0;
radio-input
Defined in Radio/Radio.tsx
opacity: 0;
width: ${space[5]};
height: ${space[5]};
margin: 0;
padding: 0;
z-index: 1;
cursor: pointer;
radio-label
Defined in Radio/Radio.tsx
padding-left: ${space[2]};
align-self: center;
cursor: pointer;
&:before,
&:after {
content: '';
position: absolute;
border-radius: 50%;
}
&:before {
top: ${(space[5] - space[3]) / 2};
left: 0;
width: ${space[5]};
height: ${space[5]};
border: ${borderWidths.standard} solid ${colors.border};
background: ${colors.canvas};
}
&:after {
top: ${(space[7] - space[5]) / 2 + (space[5] - space[3]) / 2};
left: ${(space[5] - space[3]) / 2};
width: ${space[3]};
height: ${space[3]};
background: ${colors.primary};
opacity: 0;
}
input:checked + &:after {
opacity: 1;
}
input:focus-visible + &:before {
${focusRing}
}
input[data-error] + &:before,
input[aria-invalid] + &:before {
border-color: ${colors.error};
border-width: ${borderWidths.heavy};
}
layout
Defined in Radio/RadioField.tsx
display: flex;
flex-direction: ${horizontal ? 'row' : 'column'};
column-gap: ${horizontal ? space[6] : 0};
flex-wrap: wrap;
In brief
Radio is a native radio input with a visible label. A screen reader announces the group label, then the option label, the role radio button and whether it is selected.
What Nebula provides
- A native input of type radio, hidden by making it transparent, so it is still the control that takes focus and is announced.
- Radios that share a name form one group, so the arrow keys move between them and select. RadioField gives every Radio its name.
- The label is linked to the input, so clicking it selects the radio, and a hint is linked as a description, not as part of the name.
- RadioField wraps the group in a fieldset with a legend that carries the label and hint.
Keyboard behaviour
A group is one tab stop, reached with Tab. The arrow keys move between its options.
Interactions
- Tab - Moves focus into the group, to the selected option or the first
- Up, Down, Left or Right - Moves focus to the next or previous option and selects it
Accessible design
Give every group of radios a label
The label is read before each option, so "Football" is heard as an answer to "What is your favourite sport?". Use RadioField and write a label that says what the user is choosing.
WCAG: 1.3.1 Info and Relationships 3.3.2 Labels or Instructions
Put a hint on an option only where the label is not enough
A hint is read as a description after the option's name, so a hint on every option makes each one slow to listen to. Leave it off the options that are clear.
WCAG: 3.3.2 Labels or Instructions
Accessible development
Always pass a unique id to each Radio
The id is what links the label and hint to the input. Without a unique one the radio has no name, or takes the name of another.
WCAG: 1.3.1 Info and Relationships 4.1.2 Name, Role, Value
If answering is not required, set optional on RadioField
Without it every radio in the group is marked as required to a screen reader, and the label does not say the group is optional.
WCAG: 3.3.2 Labels or Instructions
Always render Radio as a direct child of RadioField
The field sets the name, the selected state and the error state on its direct children only. A radio inside a wrapper element misses them and drops out of the group.
WCAG: 1.3.1 Info and Relationships
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.