Skip to main content

RadioGroup

Implemented

Radio group container — wraps Radio atoms with shared name, controlled value, roving tabindex, and arrow-key navigation. Provides role="radiogroup" semantics.

Categorymolecules
Sourcepatterns/molecules/radio-group.html
AI HintUse for any single-select option group (2+ options). Compose with Radio atoms. For binary choices, consider Switch instead.

Grouped 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

Please select an option

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) and aria-required when required.
  • Options are native input[type="radio"] inputs with label htmlFor association and React.useId-derived ids; touch targets are at least 44px (--ep-tap-min) and every input has a focus-visible ring.
  • 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 via aria-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

Prop
Type
Default
Description
name*
string
—

Native radio group name shared by every option input.

value*
string | null
—

Controlled selected value — the value of the selected option, or null when nothing is selected.

onChange*
(value: string) => void
—

Called 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'
vertical

Layout direction of the option list.

ariaLabel*
string
—

Accessible name for the radiogroup container (ignored when ariaLabelledBy is set).

ariaLabelledBy
string
—

Id of an element that labels the group; overrides ariaLabel when provided.

error
string
—

Error message rendered in a role="alert" caption below the options.

required
boolean
false

Marks the group required: aria-required on the container plus a native required flag on the first input.

className
string
—

Additional classes appended to the group container.

Props

Preview

RadioGroup

Prop
Type
Required
Default
Description
name
string
Required
—

Shared name for all radio inputs in the group

value
string | null
Required
null

Currently selected option value, or null if nothing selected

options
RadioGroupOption[]
Required
—

Array of { value, label, disabled? } objects

orientation
'vertical' | 'horizontal'
Optional
'vertical'

Layout direction

ariaLabel
string
Required
—

Accessible label for the group

error
string
Optional
undefined

Error message displayed below the group

onChange
(value: string) => void
Required
—

Called when the value changes.

ariaLabelledBy
string
Optional
—

Id of an element that names the group (alternative to ariaLabel).

required
boolean
Optional
false

Marks the field as required.

className
string
Optional
—

Class appended to the radiogroup container.

Quick use:<RadioGroup />

Accessibility

ARIA roleradiogroup

ARIA

1 rule

WAI-ARIA Radio Group Pattern

role='radiogroup' with aria-label; children have implicit role=radio

a11y

2 rules

WCAG 2.1.1

roving tabindex: selected option tabIndex=0, others tabIndex=-1

WAI-ARIA Radio Pattern

ArrowUp/Down/Left/Right navigate options with wrapping

Usage

Import

tsxSnippet
import { RadioGroup } from "@epasslive/design-system"

Example 1

tsxSnippet
<RadioGroup name="mode" value={mode} onChange={setMode} options={options} ariaLabel="Display mode" />

Example 2

tsxSnippet
<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

Design System v4.1.0 | ePassLive