RadioGroup

Picks exactly one option from a short, visible set. The group has a legend, each option a label, and the whole group one error message.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { RadioGroup } from '@design-schema/react';

Examples

Shipping method

Free, 3 to 5 business days

Next business day

Ready in 2 hours

import { RadioGroup } from '@design-schema/react';

<RadioGroup
  label="Shipping method"
  name="shipping"
  options={[
    { value: 'standard', label: 'Standard', description: 'Free, 3 to 5 business days' },
    { value: 'express', label: 'Express', description: 'Next business day' },
    { value: 'pickup', label: 'Pick up in store', description: 'Ready in 2 hours' },
  ]}
/>

Props

11 rows
PropTypeDefaultDescriptionPlatforms
label requiredstring—

The group's legend — the question the options answer. Always visible.

Accessibility: Rendered as the fieldset legend on web; as the group's accessibilityLabel on native.

web, lit, rn, swiftui
name requiredstring—

Field name used by the enclosing Form. Also links the radios into one native group on web.

web, lit, rn, swiftui
options requiredarray { value: string; label: string; description?: string; disabled?: boolean }[]—

The options in display order. Two to about seven; more than that is a Select (planned). Values are short identifiers (letters, digits, dashes) — they become element ids. Export the item type as `RadioGroupOption`.

web, lit, rn, swiftui
valuestring—

Controlled selected value. Omit for an uncontrolled group.

web, lit, rn, swiftui
defaultValuestring—

Initial selection for an uncontrolled group. Omit to start with nothing selected. It is read once, when the group first renders: assigning it later — routine on a Lit element, whose properties are often set after upgrade — is a no-op, exactly as a React state initialiser is.

web, lit, rn, swiftui
orientationvertical | horizontalvertical

Layout of the options. Horizontal only for two or three short labels; it wraps rather than overflows.

web, lit, rn, swiftui
requiredbooleanfalse

An option must be selected to submit. Shown in the legend, not only by color.

web, lit, rn, swiftui
invalidbooleanfalse

Marks the group as failing validation. Usually set by the Form; can be set directly.

web, lit, rn, swiftui
disabledbooleanfalse

Disables every option. Individual options use `options[].disabled`. The group's state folds into each option's, so every radio announces as disabled rather than only the group. A disabled group submits nothing and validates clean — the form value is cleared and no validity is set, as a native disabled control does — so a disabled required group never blocks a submit.

web, lit, rn, swiftui
descriptionstring—

Persistent helper text under the legend.

Accessibility: Linked to the group with aria-describedby / accessibilityHint.

web, lit, rn, swiftui
errorstring—

The group's error message. Setting it marks the group invalid.

Accessibility: Rendered once under the group with role=alert and linked with aria-describedby on the group.

web, lit, rn, swiftui

Events

1 row
EventTypeDescriptionPlatforms
onChangeonChange / change

Fired when the selection changes, with the new option value.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply the label. It is associated with the control for you, but the words are yours — a placeholder is not a label.

Supply the error text when the value is invalid. The component links and announces it; it cannot write it.

Required props that carry accessibility meaning: label. Each is described in the props table above.

Implicit ARIA role: radiogroup.

Anatomy

grouplegenddescriptionradioradioIndicatorradioLabelradioDescriptionerrorMessage

What generation guarantees

Label association — The label is programmatically associated with the control, not merely placed beside it.

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Roving tabindex — The group is a single tab stop; the arrow keys move within it.

Error identification — An invalid value is identified in text and linked to the control — never signalled by colour alone.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

RadioGroup keyboard support

4 rows
KeyAction
TabMoves into the group, to the selected radio (the first when none is selected); from inside, leaves the group — one tab stop.
ArrowDown or ArrowRightMoves to and selects the next enabled radio, wrapping.
ArrowUp or ArrowLeftMoves to and selects the previous enabled radio, wrapping.
SpaceSelects the focused radio when the arrows did not already select it.

Contrast pairs

6 rows
ForegroundBackgroundLevel
color.control.selectedBackgroundcolor.control.backgroundAA (non-text)
color.control.selectedBackgroundcolor.backgroundAA (non-text)
color.control.bordercolor.backgroundAA (non-text)
color.foregroundcolor.backgroundAA
color.foreground.mutedcolor.backgroundAA
color.foreground.dangercolor.backgroundAA

This component does not animate, so it has no reduced-motion behaviour to describe.