---
title: Radio Group
description: Radio groups present mutually exclusive options. Only one choice can be selected at a time within the group.
---

import { meta as RadioGroupMeta, Default } from '@/content/stories/radio-group';
import Canvas from '@/components/component-docs/canvas';
import Demo from '@/components/component-docs/demo';

export const config = {
	dependencies: {
		components: [Canvas, Demo],
		scripts: [
			'../../../components/component-docs/demo.script.tsx',
			'../../../components/component-docs/canvas.script.tsx',
			'../../../components/component-docs/controls.script.tsx',
		],
	},
};


# Radio Group

<p class="docs-lede">Radio groups present mutually exclusive options. Only one choice can be selected at a time within the group.</p>

## Try it

<Demo of={Default} meta={RadioGroupMeta} />

## Usage

Set `value` for the selected option and provide radio inputs as children. Wrap in `RuiField` for form integration. Set `name` on the group (or the field) to list inner radios on native `FormData`.

```tsx
import { RuiRadio, RuiRadioGroup, RuiRadioGroupControl } from '@ecopages/radiant-ui/radio-group';
import { RuiField } from '@ecopages/radiant-ui/field';
import { RuiLabel } from '@ecopages/radiant-ui/label';

<RuiField name="plan">
  <RuiLabel>Plan</RuiLabel>
  <RuiRadioGroup value="pro" name="plan" label="Plan">
    <RuiRadioGroupControl>
      <RuiRadio value="free">Free</RuiRadio>
      <RuiRadio value="pro">Pro</RuiRadio>
    </RuiRadioGroupControl>
  </RuiRadioGroup>
</RuiField>
```

## Custom markup

`<rui-radio-group>` coordinates any light-DOM tree that matches its query contract. The `Rui*` helpers stamp these targets; they are not required.

```tsx
import '@ecopages/radiant-ui/radio-group';

<rui-radio-group value="pro" name="plan" label="Plan">
  <div data-radio-group-root role="radiogroup" class="rui-radio-group">
    <label class="rui-radio">
      <input type="radio" class="rui-radio__input" value="free" />
      <span class="rui-radio__control" aria-hidden="true"></span>
      <span class="rui-radio__label">Free</span>
    </label>
    <label class="rui-radio">
      <input type="radio" class="rui-radio__input" value="pro" />
      <span class="rui-radio__control" aria-hidden="true"></span>
      <span class="rui-radio__label">Pro</span>
    </label>
  </div>
</rui-radio-group>
```

BEM classes are presentation-only. Do not set `name`, `checked`, or `disabled` on radios — the host owns those.

## Mutually exclusive choices

<p>Use radio groups when options are mutually exclusive. For independent toggles that share one field value, use a <a href="/docs/forms/checkbox-group">checkbox group</a>; for a single on/off toggle, use a <a href="/docs/forms/checkbox">checkbox</a>.</p>

## Theming

Radio group surfaces map to **semantic roles**, never Tailwind palette steps:

| Part | CSS roles |
| --- | --- |
| Option row (`.rui-radio`) | `on-background` text, `space-inline` gap |
| Control dot (`.rui-radio__control`) | `border`, `background`; `primary` when checked |
| Checked dot (`::after`) | `primary` fill |
| Focus ring | `focus-ring` |
| Disabled | `opacity-disabled` |

The custom control dot is decorative (`aria-hidden`), the visually hidden native radio input carries the accessible state. Override at the theme layer, not in component CSS.

## API

`RuiRadioGroup` is a custom element (`<rui-radio-group>`). Compose its options with `RuiRadioGroupControl` and `RuiRadio`, or use the convenience `options` prop to generate the same structure.

### Attributes

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | `''` | Selected radio value. |
| `name` | `string` | `''` | Form field name shared by all radios in the group. |
| `label` | `string` | `''` | Accessible name when no visible legend is composed in the view. |
| `disabled` | `boolean` | `false` | Disables every radio in the group. |

### Light-DOM contract

| Target | Required | Host writes | Author owns |
| --- | --- | --- | --- |
| `[data-radio-group-root]` | yes | `aria-label`, `aria-disabled` | the group node |
| `input[type="radio"]` | yes (per option) | `name`, `checked`, `disabled` | `value`, `data-disabled` |

Do not set `name`, `checked`, or `disabled` on radios. Nested hosts: none.

### Events

| Event | Detail | Description |
| --- | --- | --- |
| `rui-change` | `{ value: string }` | Emitted after the selected value changes. |

### Props

| Prop | Type | Description |
| --- | --- | --- |
| `options` | `RuiRadioOption[]` | `{ value, label, disabled? }` entries rendered by the view. |

### View helpers

| Component | Target stamped | Notes |
| --- | --- | --- |
| `RuiRadioGroup` | `<rui-radio-group>` | Accepts `options` or children. |
| `RuiRadioGroupControl` | `[data-radio-group-root]` | `role="radiogroup"` container. |
| `RuiRadio` | `input[type="radio"]` | Label-wrapped option with `value`. |

### CSS classes

Public BEM classes (documented via `@cssclass`):

| Class | Description |
| --- | --- |
| `.rui-radio-group` | Group surface (`role="radiogroup"`). |
| `.rui-radio` | Option label row (authored by the `RuiRadioGroup` view). |
| `.rui-radio__input` | Visually hidden native radio input. |
| `.rui-radio__control` | Custom control dot (mirrors check state). |
| `.rui-radio__label` | Option text. |

### Theme roles

| Part | CSS variables consumed |
| --- | --- |
| Option row | `--on-background`, `--space-inline` |
| Control dot | `--border`, `--background`, `--primary` |
| Focus | `--focus-ring` |
| Disabled | `--opacity-disabled` |

## Accessibility

- The group exposes `role="radiogroup"` with an accessible name from `label`.
- Arrow keys move selection between radio options.
- Visually indicate the selected option; do not rely on position alone.
