# RadioGroup

**When to use:** Single-choice selection from a small, always-visible set of options — plan pickers, shipping methods, yes/no/maybe, settings with 2–5 mutually exclusive choices. Prefer `Dropdown` when the list is long, and `OptionPicker` for a compact segmented control.

**Keywords:** radio buttons

**Import:** `import { RadioGroup, RadioGroupOrientation } from '@ahrowe/ui'`
**Types:** `import type { RadioOption, RadioGroupProps, RadioValue } from '@ahrowe/ui'`

Follows the ARIA `radiogroup` pattern: one roving tab stop, arrow keys move focus **and** selection (skipping disabled options and wrapping around), Space/Enter selects the focused option.

```tsx
import { RadioGroup, RadioGroupOrientation, FormValidator } from '@ahrowe/ui';
import type { RadioOption } from '@ahrowe/ui';

const options: RadioOption[] = [
  { value: 'free', label: 'Free' },
  { value: 'pro', label: 'Pro' },
  { value: 'team', label: 'Team', disabled: true },
];

// Controlled
<RadioGroup options={options} value={plan} onChange={(v) => setPlan(v as string)} />

// Uncontrolled with an initial value
<RadioGroup options={options} defaultValue="free" />

// Horizontal layout
<RadioGroup options={options} value={plan} onChange={setPlan} orientation={RadioGroupOrientation.Horizontal} />

// Per-option descriptions
<RadioGroup
  options={[
    { value: 'standard', label: 'Standard', description: '3–5 business days, free' },
    { value: 'express', label: 'Express', description: 'Next business day, €9.90' },
  ]}
  value={shipping}
  onChange={setShipping}
/>

// With FormValidator
const planValidator = new FormValidator<string | null>(null, [Validators.required()]);
<RadioGroup options={options} formValidator={planValidator} />
```

**RadioGroupOrientation enum:** `RadioGroupOrientation.Vertical` (default) | `RadioGroupOrientation.Horizontal`

**RadioOption:**

```ts
interface RadioOption {
  value: string | number;
  label?: ReactNode;        // falls back to `value`
  description?: ReactNode;  // secondary text under the label
  disabled?: boolean;       // disable just this option
}
```

**Naming the group:** `label` shows above the options, names the group for screen readers and
gives its validator's messages the field's name ("Plan is required"). Without a visible label,
name it with `aria-label` instead.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `options` | `RadioOption[]` | The selectable options |
| `label` | `string` | Shown above the options; names the group and the field in its validator's messages |
| `value` | `string \| number` | Controlled selected value |
| `defaultValue` | `string \| number` | Initial value when uncontrolled |
| `onChange` | `(value, event?) => void` | Fires with the newly selected value |
| `orientation` | `RadioGroupOrientation` | Layout direction (default `Vertical`) |
| `disabled` | `boolean` | Disable the whole group |
| `size` | `string` | Diameter of the radio circle. Defaults to `1.125em` of `--default-font-size`, i.e. 18px at a 16px base |
| `formValidator` | `FormValidator` | Connects to form validation |
| `errorMessage` | `string` | Manual error message (when not using a `formValidator`). A non-empty message puts the field in error by itself, so pass it only while there is one |
| `isValid` | `boolean` | Manual valid state (default `true`) |

**Validation:** like `Input`, an errored group shows an error `Tooltip` and turns its unselected circles red. With a `formValidator` the error surfaces once the validator's `showErrors` allows it (by default after the field is *touched*: focus leaves the group, see FormValidator.md), then on focus or hover.

**Global defaults:** adopts `ConfigProvider` — e.g. `defaultProps={{ RadioGroup: { orientation: RadioGroupOrientation.Horizontal } }}`. See [ConfigProvider.md](ConfigProvider.md).

**Slots:** `root` `groupLabel` `options` `option` `radio` `dot` `label` `description`

## Sizing

The group is sized by its font, so the radios, the spacing between options and the gap to each label all scale with the text. `--radio-size` and `--radio-gap` override the circle and the label spacing; they fall back to `1.125em` and `0.625em`, i.e. 18px and 10px at a 16px base. The dot is about half the circle.

The `size` prop still wins where it is given, for a radio that must match a fixed layout rather than the text around it.
