RadioGroup
ImplementedRadio group container — wraps Radio atoms with shared name, controlled value, roving tabindex, and arrow-key navigation. Provides role="radiogroup" semantics.
patterns/molecules/radio-group.htmlGrouped radio list for picking exactly one option from a small set. Renders native radio inputs sharing one name, with roving-tabindex keyboard support, an optional per-option disabled state, and a role="alert" error slot. Composes the Radio and Caption atoms.
RadioGroup
Form selection molecule — Family 3
Controlled radio group: the parent owns the selected value and reacts to onChange. The container exposes role="radiogroup" with an accessible name (ariaLabel or ariaLabelledBy). Prefer it over hand-rolled role="radiogroup" wrappers around Radio atoms — the molecule owns the shared name, the roving tabindex, and the error caption.
Vertical (default)
Horizontal
With error
With disabled option
This component supports light and dark mode.
Usage
RadioGroup is a controlled component — the parent owns the selected value and reacts to onChange. Give the group an accessible name via ariaLabel(or ariaLabelledBy when a visible heading labels it). Options support a per-option disabled flag for frozen or unavailable choices.
import { useState } from 'react'
import { RadioGroup } from '@epasslive/design-system'
const MODES = [
{ value: 'none', label: 'None (do not collect)' },
{ value: 'optional', label: 'Optional (buyer can skip)' },
{ value: 'mandatory', label: 'Mandatory (required per ticket)' },
]
const [mode, setMode] = useState<string | null>('optional')
<RadioGroup
name="holder-name-mode"
value={mode}
onChange={setMode}
options={MODES}
ariaLabel="Holder name collection mode"
/>Keyboard interaction
Tab— enters the group at the selected option (roving tabindex: exactly one option carries tabIndex 0).ArrowUp/ArrowDown/ArrowLeft/ArrowRight— move focus between options, wrapping at the ends.Home/End— focus the first / last option.Space— selects the focused option. A mouse click also commits a selection.
Keyboard behavior (current): arrow keys move focus and select the newly focused option (WAI-ARIA APG radio-group behavior), navigation wraps at the ends and skips disabled options, and with value === null the first option is the group's single tab stop. Exactly one option carries tabIndex=0 at a time (strict roving tabindex). Click and Space also commit a selection.
Accessibility notes
- The container exposes
role="radiogroup"with an accessible name (ariaLabel/ariaLabelledBy— mutually exclusive) andaria-requiredwhen required. - Options are native
input[type="radio"]inputs withlabel htmlForassociation andReact.useId-derived ids; touch targets are at least 44px (--ep-tap-min) and every input has afocus-visiblering. - The error message renders in a
role="alert"caption. Known gap: it inherits the muted caption color and is not yet wired to the group viaaria-describedby. - Known contrast caveat: the teal control outline on the light card surface measures ≈2.95:1, marginally under the 3:1 WCAG 1.4.11 non-text minimum (dark theme passes at ≈4.74:1). A dedicated interactive outline token is under review.
Props
name*stringNative radio group name shared by every option input.
value*string | nullControlled selected value — the value of the selected option, or null when nothing is selected.
onChange*(value: string) => voidCalled with the newly selected option value. Fires on click and Space.
options*RadioGroupOption[]Options to render. Each has value, label, and an optional disabled flag.
orientation'vertical' | 'horizontal'verticalLayout direction of the option list.
ariaLabel*stringAccessible name for the radiogroup container (ignored when ariaLabelledBy is set).
ariaLabelledBystringId of an element that labels the group; overrides ariaLabel when provided.
errorstringError message rendered in a role="alert" caption below the options.
requiredbooleanfalseMarks the group required: aria-required on the container plus a native required flag on the first input.
classNamestringAdditional classes appended to the group container.
Props
RadioGroup
namestring—Shared name for all radio inputs in the group
valuestring | nullnullCurrently selected option value, or null if nothing selected
optionsRadioGroupOption[]—Array of { value, label, disabled? } objects
orientation'vertical' | 'horizontal''vertical'Layout direction
ariaLabelstring—Accessible label for the group
errorstringundefinedError message displayed below the group
onChange(value: string) => void—Called when the value changes.
ariaLabelledBystring—Id of an element that names the group (alternative to ariaLabel).
requiredbooleanfalseMarks the field as required.
classNamestring—Class appended to the radiogroup container.
<RadioGroup />Accessibility
ARIA
1 rulea11y
2 rulesUsage
Import
import { RadioGroup } from "@epasslive/design-system"Example 1
<RadioGroup name="mode" value={mode} onChange={setMode} options={options} ariaLabel="Display mode" />Example 2
<RadioGroup name="delivery" value={method} onChange={setMethod} options={deliveryOptions} orientation="horizontal" ariaLabel="Delivery method" />Anti-patterns
— Do not use Radio atoms outside a RadioGroup
— Do not duplicate aria-label on both group and individual radios