# Radio group

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/radio-group
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-3

```tsx
import { RadioGroup } from '@commenda/pathfinder/radio-group';

<RadioGroup
  label="Filing frequency"
  name="frequency"
  value={frequency}
  onValueChange={setFrequency}
  options={[
    { value: 'annual', label: 'Annual' },
    { value: 'quarterly', label: 'Quarterly' },
    { value: 'monthly', label: 'Monthly' },
  ]}
/>;
```


## Installation [#installation]

```sh
pnpm --filter your-app add "@commenda/pathfinder@workspace:*"
```

Import `@commenda/pathfinder/styles.css` once at the app root. The stylesheet is precompiled with Tailwind v4; your application does not need Tailwind or source scanning. Event handlers belong in a client component in Next.js App Router.

## Usage [#usage]

```tsx
import { RadioGroup } from '@commenda/pathfinder/radio-group';

<RadioGroup
  label="Filing frequency"
  name="frequency"
  value={frequency}
  onValueChange={setFrequency}
  options={[
    { value: 'annual', label: 'Annual' },
    { value: 'quarterly', label: 'Quarterly' },
    { value: 'monthly', label: 'Monthly' },
  ]}
/>;
```

| Prop                   | Type                         | Default / behavior                                               |
| ---------------------- | ---------------------------- | ---------------------------------------------------------------- |
| `label`                | `string`                     | Required group name, visible by default                          |
| `hideLabel`            | `boolean`                    | `false`; hides only the visible group heading                    |
| `description`          | `string`                     | Optional group guidance                                          |
| `options`              | `readonly RadioOption[]`     | Required; stable, unique string values                           |
| `value`                | `string \| null`             | Controlled selected value; `null` means intentionally unanswered |
| `defaultValue`         | `string`                     | Uncontrolled initial selection; omitted means unanswered         |
| `onValueChange`        | `(value: string) => void`    | Required with controlled `value`, optional when uncontrolled     |
| `variant`              | `'default' \| 'card'`        | `'default'`                                                      |
| `orientation`          | `'vertical' \| 'horizontal'` | `'vertical'`; horizontal layout wraps and cards share row height |
| `disabled`             | `boolean`                    | Disables interaction for the control or group.                   |
| `readOnly`             | `boolean`                    | Keeps the current value visible while preventing changes.        |
| `required`             | `boolean`                    | Requires a value for native form validation.                     |
| `name`                 | `string`                     | Name used when the control submits its value with a form.        |
| `form`                 | `string`                     | ID of the form associated with the control.                      |
| `inputRef`             | `Ref<HTMLInputElement>`      | Access to the underlying form input.                             |
| `className`            | `string`                     | Additional class names for the outer layout.                     |
| `ref`                  | `Ref<HTMLDivElement>`        | Access to the interactive root element.                          |
| Native events and ARIA | Base UI group props          | Typed passthrough                                                |

`RadioOption`:

| Property         | Type                | Purpose                                                            |
| ---------------- | ------------------- | ------------------------------------------------------------------ |
| `value`          | `string`            | Required stable, unique identity and submitted value               |
| `label`          | `string`            | Required option name                                               |
| `description`    | `string`            | Supporting explanation                                             |
| `details`        | `readonly string[]` | Additional card bullets                                            |
| `disabled`       | `boolean`           | Unavailable option; keyboard navigation skips it                   |
| `disabledReason` | `string`            | Visible and associated while the option or whole group is disabled |

Use two to five visible options. Prefer a sensible default, but permit an empty group when explicitly choosing is necessary. Do not auto-select the first item or silently replace a removed controlled value. The owner reconciles changing options. Labels select their option; arrow keys move selection, and only one value submits. For longer lists use Select. Never use one radio for a boolean setting.

Radio cards use `variant="card"`; they are the same radio group, not an unrelated clickable card API. Figma calls its collection “Radio card” but includes checkbox cards; those belong to Checkbox to preserve multiple-selection semantics. Cards have 16px padding, 12px control/text gap, 8px radius and 13px medium labels. Their width adapts from the 320px Figma specimen.

## Examples [#examples]

### Descriptions [#descriptions]


```tsx
<RadioGroup
  label="Filing frequency"
  defaultValue="annual"
  options={[
    { value: 'annual', label: 'Annual', description: 'One return per year.' },
    {
      value: 'quarterly',
      label: 'Quarterly',
      description: 'Four returns per year.',
    },
  ]}
/>;
```


### Radio cards [#radio-cards]


```tsx
<RadioGroup
  label="Filing frequency"
  variant="card"
  orientation="horizontal"
  defaultValue="annual"
  options={[
    {
      value: 'annual',
      label: 'Annual',
      description: 'One return at the end of the fiscal year.',
      details: ['Due 15 April'],
    },
    {
      value: 'quarterly',
      label: 'Quarterly',
      description: 'One return every three months.',
    },
    {
      value: 'monthly',
      label: 'Monthly',
      description: 'One return every month.',
    },
  ]}
/>;
```


### Disabled group [#disabled-group]


```tsx
<RadioGroup
  label="Filing frequency"
  defaultValue="annual"
  disabled
  options={[
    { value: 'annual', label: 'Annual' },
    { value: 'quarterly', label: 'Quarterly' },
    {
      value: 'monthly',
      label: 'Monthly',
      disabledReason: 'Unavailable on the current plan',
    },
  ]}
/>;
```


Read the [Figma choice-control rules](/docs/components/radio-group#design-rules) for design guidance, source discrepancies and accessibility requirements.

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Radio and cards [#radio-and-cards]

Source: [199:3](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-3), radio variants 200:46, card variants 201:413, guidance 205:320.

- **RADIO-01** — One choice from two to five visible options. Use Select for longer lists or limited space. A lone boolean option uses Checkbox/Switch. Arrow keys move selection and skip disabled items; Tab enters/leaves the group.
- **RADIO-02** — Prefer a sensible default. Leave the group unanswered only when deliberate choice is required. Never silently choose the first value or replace a removed controlled value. The application reconciles dynamic options.
- **RADIO-03** — Order options by frequency, size or time rather than alphabetically by default. Circle 16px, selected white dot 6px; colors and focus match Checkbox. Group row gap 8px.
- **CARD-01** — Use a card when each option needs title and description. Keep cards in one row equal-height. The “Radio card” Figma collection contains both radio and checkbox controls; Pathfinder exposes `RadioGroup variant="card"` for one choice and `Checkbox variant="card"` for independent choices.
- **CARD-02** — Card padding 16px, control/text gap 12px, radius 8px. Title Label/13 Strong; description Copy/12 with 4px gap. Detail bullets have 2px gap and 4px top padding. Surface remains white when selected; border becomes blue-700. Focus surrounds the whole card. Disabled surface/border/text use soft-pearl-100/300/600. The 320px specimen width adapts to the container.

### Implementation decisions and source differences [#implementation-decisions-and-source-differences]

The complete prop tables are on [Checkbox](/docs/components/checkbox), [RadioGroup](/docs/components/radio-group) and [Switch](/docs/components/switch). Source rule IDs are also kept in the component implementation comments and exported through `llms.txt`, `llms-full.txt` and the Markdown routes.

- **CHOICE-01** — The sheets describe 8px list gaps but some documentation frames place rows 32px apart. Use the documented 8px component-list gap; documentation specimen whitespace is not a new size token.

- **CHOICE-02** — Built-in text is plain, wrapping content. Do not place links, buttons or nested interactive controls inside labels or cards. Disabled reasons are visible descriptions, not tooltip-only explanations on controls removed from keyboard navigation.

- **CHOICE-03** — Visual control size is not the hit target. Coarse-pointer rows provide at least 44px height, while preserving 16px marks and the 36×20px switch track. Cards already exceed this height.

- **CHOICE-04** — Hover/focus are real browser states, not public cosmetic props. Retain native `name`, `value`, `form`, `required`, `readOnly`, disabled semantics and input refs. `hideLabel` preserves the required accessible label.

- **CHOICE-05** — Use Pathfinder's existing Check/Minus optical artwork at 12px. Boxes, circles, tracks and thumbs are interactive primitive geometry, styled with the Figma tokens; they are not flattened screenshots. This keeps focus, RTL and switch movement functional.
