Components
Radio Button
BetaRadio buttons let users select exactly one option from a set. Use RadioGroup to render the full accessible group with a fieldset and legend.
Purpose
Radio buttons enforce single selection from a mutually exclusive set. Once one option in a group is chosen, all others are deselected. This is the key distinction from checkboxes, where any number of options may be active simultaneously.
Use radio buttons when:
- Exactly one answer is valid from a small, exhaustive set (3–7 options)
- The options need to be visible side by side so the user can compare them before deciding
- A default must be pre-selected to represent the most common choice
If the list exceeds approximately 7 options, consider a select dropdown to save vertical space. If all options are valid simultaneously (extras, preferences), use checkboxes. For an immediate on/off action without a form submit, use the Toggle component.
This file exports two components:
RadioGroup— the recommended way to render a complete accessible group withfieldset,legend, and error handling. Use this in almost all cases.RadioButton— a single radio input. Only use it directly when building a fully custom layout whereRadioGroupcannot be composed.
Usage
Reach for RadioButton when exactly one option must be chosen from a small, visible set. If any number of options can be true at once, use Checkbox instead. If the interaction is a single on/off setting rather than a choice from a set, use Toggle.
RadioGroup typically appears in booking forms — room type, insurance level, trip duration — wherever the traveller must commit to exactly one of a handful of comparable options.
Anatomy
- 1Fieldset — Native HTML fieldset groups the radio inputs semantically. The border is reset to none in the component.
- 2Legend — The group question or label. Announced by screen readers before each radio in the group.
- 3Control circle — 16×16 px circle (rounded-full). Unchecked: white background, grey-300 border. Checked: teal border with a white inset shadow that creates the classic filled-dot appearance.
- 4Option label — Sentence-case text tied to the radio via htmlFor/id.
- 5Description — Optional secondary text beneath the option label. Useful for pricing or extra detail.
- 6Error message — Validation text shown below the group when the error prop is set on RadioGroup.
- 7Focus ring — 2 px teal ring with 2 px offset, visible on keyboard focus only (focus-visible).
Variants
Vertical group (default)
The default orientation stacks options vertically with gap-3. Vertical groups are easiest to scan when options have descriptions or when labels are more than three words long.
Horizontal group
Pass orientation="horizontal" to lay the options out in a row. Best for short, equal-length labels (2–4 options). Wraps to multiple lines on narrow viewports.
With descriptions
Option-level description strings add secondary text beneath each label. Use for pricing, benefit summaries, or clarifying detail that helps the user decide.
States
Unchecked and checked
The unchecked circle has a border-grey-300 border on a white background. The checked circle gains a teal border and a white inset box-shadow that creates the filled-dot appearance without a separate inner element. This approach works correctly at all DPI levels.
Focus
A 2 px teal focus ring with 2 px offset appears on :focus-visible. The ring surrounds the 16 px circle only, not the label. Mouse clicks do not trigger the ring.
Disabled
Individual options can be disabled via the disabled key in the options array. The entire group can be disabled via the disabled prop on RadioGroup. Disabled options are rendered at reduced opacity with cursor-not-allowed.
Error state
Pass an error string to RadioGroup to show a validation message below the group. The error text is wired to aria-describedby on the fieldset so screen readers announce it.
Best practices
Always provide a legend to RadioGroup so screen readers can announce the question before each radio option.
The legend gives the group a shared accessible name, providing context before each option.
Render a radio group with no legend or fieldset grouping.
Without a legend, 'Economy', 'Business', and 'First class' have no shared context when navigated individually.
Use radio buttons for small, exhaustive sets of 3 to 6 mutually exclusive options.
3–6 visible options let users compare at a glance without scrolling through a long list.
Use radio buttons for lists of 7 or more options when a select dropdown would save space and scanning effort.
7 or more radio options create a long list; a select dropdown is faster to navigate.
Set a defaultValue to pre-select the most common or recommended option.
Pre-selecting the most popular room type reduces friction for the majority of users.
Leave a radio group with no pre-selected value when one option is clearly the most common choice.
An empty group forces every user to make an active selection even when one answer is overwhelmingly typical.
Write error messages that describe what the user must do: "Please select a cabin class to continue."
Specific error messages tell users what action is needed to continue.
Use a vague error message like "Required" or "Invalid selection."
'Required' alone gives no actionable direction.
Props
RadioGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
| legend | string | — | The fieldset legend text. Announced by screen readers as the group label before each option. |
| name* | string | — | Shared name attribute for all radio inputs in the group. Required for browser grouping and form serialisation. |
| options* | Array of { value, label, description?, disabled?, id? } | — | Array of option objects. Each renders one RadioButton. Provide value and label at minimum. |
| value | string | — | Controlled selected value. When provided, the group is fully controlled and requires an onChange handler. |
| defaultValue | string | — | Uncontrolled initial selected value. Pre-selects the matching option on first render. |
| onChange | (value: string, option: object) => void | — | Called when the user selects a new option. Receives the selected value string and the full option object. |
| orientation | 'vertical' | 'horizontal' | 'vertical' | Layout direction of the options. Horizontal wraps to multiple lines on narrow viewports. |
| disabled | boolean | false | Disables all options in the group. Individual options can also be disabled via the disabled key in the options array. |
| required | boolean | false | Marks the group as required. Adds a visually-hidden "(required)" text for screen readers. |
| error | string | — | Validation error message shown below the group. Wired to aria-describedby on the fieldset. |
| helpText | string | — | Guidance text below the group. Hidden when error is set. |
| className | string | — | Additional Tailwind classes on the root fieldset element. |
RadioButton props (single item)
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | — | The value submitted with the form and used to identify the selected option. |
| label* | string | — | Visible label text tied to the radio via htmlFor/id. |
| name | string | — | Groups radio inputs in the browser. Required when using RadioButton directly (outside of RadioGroup). |
| id | string | — | HTML id. Auto-generated when omitted. |
| description | string | — | Secondary text rendered beneath the label. |
| checked | boolean | — | Controlled checked state. |
| defaultChecked | boolean | false | Uncontrolled initial checked state. |
| disabled | boolean | false | Disables this option. Applies reduced opacity and not-allowed cursor. |
| required | boolean | false | Marks the input as required for browser validation. |
| onChange | (event: ChangeEvent) => void | — | Change handler for controlled usage. |
| className | string | — | Additional Tailwind classes on the root wrapper. |
Accessibility
Keyboard interaction
| Key | Action |
|---|---|
Tab | Move focus into the radio group (lands on the checked option, or the first option if none checked) |
Shift + Tab | Move focus out of the group to the previous interactive element |
Arrow Down / Arrow Right | Select the next option in the group (wraps to first at end) |
Arrow Up / Arrow Left | Select the previous option (wraps to last at beginning) |
Space | Select the focused option if it is not already selected |
Radio buttons use the native arrow-key roving-tabindex pattern. Only one radio in the group is in the tab order at a time — the currently selected one, or the first if nothing is selected. This prevents Tab from stopping on each option individually.
Fieldset and legend
The fieldset / legend combination is the HTML-native grouping mechanism for radio buttons. Without it, screen readers announce each option in isolation with no shared question context. The RadioGroup component always renders a fieldset; the legend renders even when the legend prop is omitted (it is visually hidden but present for assistive technology).
Error announcement
When error is set on RadioGroup:
aria-describedbyon the fieldset points to the error paragraph.- The error paragraph has
role="alert"so the message is announced immediately when it appears.
Colour and contrast
The checked state uses a teal border (#005B55) with an inset white box-shadow for the dot. The dot is never a separate coloured element — it is expressed as negative space — so it works across all high-contrast modes and OS-level colour overrides.
| Element | Foreground | Background | Ratio | WCAG |
|---|---|---|---|---|
| Option label | #1A1A1A | White #FFFFFF | 17.4:1 | AAA |
| Description text | #666666 | White #FFFFFF | 5.9:1 | AA |
| Error text | #C0392B | White #FFFFFF | 5.4:1 | AA |