KKUONIDesign System

Components

Radio Button

Beta

Radio buttons let users select exactly one option from a set. Use RadioGroup to render the full accessible group with a fieldset and legend.

Room type

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 with fieldset, 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 where RadioGroup cannot 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

Meal plan
  1. 1
    FieldsetNative HTML fieldset groups the radio inputs semantically. The border is reset to none in the component.
  2. 2
    LegendThe group question or label. Announced by screen readers before each radio in the group.
  3. 3
    Control circle16×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.
  4. 4
    Option labelSentence-case text tied to the radio via htmlFor/id.
  5. 5
    DescriptionOptional secondary text beneath the option label. Useful for pricing or extra detail.
  6. 6
    Error messageValidation text shown below the group when the error prop is set on RadioGroup.
  7. 7
    Focus ring2 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.

Number of travellers

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.

Trip duration

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.

Travel insurance

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.

Departure airport

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.

Cabin class

Best practices

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

Use a vague error message like "Required" or "Invalid selection."

'Required' alone gives no actionable direction.

Props

RadioGroup props

PropTypeDefaultDescription
legendstringThe fieldset legend text. Announced by screen readers as the group label before each option.
name*stringShared 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.
valuestringControlled selected value. When provided, the group is fully controlled and requires an onChange handler.
defaultValuestringUncontrolled initial selected value. Pre-selects the matching option on first render.
onChange(value: string, option: object) => voidCalled 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.
disabledbooleanfalseDisables all options in the group. Individual options can also be disabled via the disabled key in the options array.
requiredbooleanfalseMarks the group as required. Adds a visually-hidden "(required)" text for screen readers.
errorstringValidation error message shown below the group. Wired to aria-describedby on the fieldset.
helpTextstringGuidance text below the group. Hidden when error is set.
classNamestringAdditional Tailwind classes on the root fieldset element.

RadioButton props (single item)

PropTypeDefaultDescription
value*stringThe value submitted with the form and used to identify the selected option.
label*stringVisible label text tied to the radio via htmlFor/id.
namestringGroups radio inputs in the browser. Required when using RadioButton directly (outside of RadioGroup).
idstringHTML id. Auto-generated when omitted.
descriptionstringSecondary text rendered beneath the label.
checkedbooleanControlled checked state.
defaultCheckedbooleanfalseUncontrolled initial checked state.
disabledbooleanfalseDisables this option. Applies reduced opacity and not-allowed cursor.
requiredbooleanfalseMarks the input as required for browser validation.
onChange(event: ChangeEvent) => voidChange handler for controlled usage.
classNamestringAdditional Tailwind classes on the root wrapper.

Accessibility

Keyboard interaction

KeyAction
TabMove focus into the radio group (lands on the checked option, or the first option if none checked)
Shift + TabMove focus out of the group to the previous interactive element
Arrow Down / Arrow RightSelect the next option in the group (wraps to first at end)
Arrow Up / Arrow LeftSelect the previous option (wraps to last at beginning)
SpaceSelect 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-describedby on 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.

ElementForegroundBackgroundRatioWCAG
Option label#1A1A1AWhite #FFFFFF17.4:1AAA
Description text#666666White #FFFFFF5.9:1AA
Error text#C0392BWhite #FFFFFF5.4:1AA
  • Checkbox — use instead when any number of options (including zero or all) may be selected simultaneously.
  • Toggle — use for a single immediate on/off setting rather than a choice from a set of options.