# RadioGroup

> One choice from a few options that people should see side by side.

- Import: `import { RadioGroup } from '@thekestongroup/ui'`
- Group: Form
- Live example: https://ui.keston.co/storybook/?path=/story/components-form-radiogroup--default
- Source: https://github.com/thekestongroup/ui/blob/main/src/components/RadioGroup/RadioGroup.vue

## When to use

- Two to five options, when seeing them all helps the choice, e.g. "Mark as leavers" / "Keep on roll".

## When not to use

- More than five options, or a minor choice: use Select.
- A yes / no question: use Checkbox (in a form) or Switch (a setting).

## Anatomy

| Part | Required | Notes |
|---|---|---|
| label | Yes | The question, above the options. The group's accessible name. |
| helper | No | Under the label, or behind a `?` (InfoTip) beside it. |
| option | Yes | A 16px circle, its label and optional description. The chosen one has an 8px primary dot. |
| message | No | Error text under the options. |

## Properties

| Property | Type | Default | Description |
|---|---|---|---|
| modelValue | string \| number | — | The chosen option's value; undefined until one is chosen. |
| label | string | — | Required. |
| options | { label, value, description?, disabled? }[] | — | Required. |
| helper | string | — | |
| helperPlacement | below \| icon | below | As on every field. |
| errorText | string | — | |
| name | string | — | For native form submission. |
| required / disabled | boolean | false | |

Options always stack vertically: no horizontal layout, so labels and descriptions never crowd.

## States

| State | Appearance | Notes |
|---|---|---|
| Not chosen | White circle, black 55% border. | |
| Hovered | Black border. | Web / desktop only. |
| Chosen | Primary border and dot. | |
| Focused | 2px focus ring, 2px out. | Keyboard only. |
| Error | Red borders, red message with icon. | |
| Disabled | 40% opacity: the whole group, or one option. | |

## Spacing and layout

- Label to helper: 2px. Label block to options: 6px. Options: 8px apart. Circle to option label: 8px.

## Behaviour

- Tab moves into the group (to the chosen option, or the first) and out again; the arrow keys move and choose.
- The whole row is pressable: the circle, its label and its description. The description is not part of the accessible name; it stays the description.
- Once chosen, an option can't be unchosen: start with nothing chosen if "none" isn't a valid answer, or add a "None" option.

## Accessibility

- `role="radiogroup"` named by the label (`aria-labelledby`), described by helper and error; `aria-invalid` on error.
- Each option is `role="radio"`, named by its `<label>`, described by its description.

## Platform adaptation

| Platform | Implementation | Differences |
|---|---|---|
| Web / desktop (Vue) | `RadioGroup.vue` | Reka UI `RadioGroup`. |
| iOS (SwiftUI) | — | `Picker` with `.inline` style in a Form. |
| Android (Compose) | — | `RadioButton` rows with `Modifier.selectableGroup()`. |

## Tokens used

| Token | Used for |
|---|---|
| `color-primary` | Chosen border and dot |
| `color-red` | Error |
| `color-focus` | Focus ring |

## Do / Don't

| Do | Don't |
|---|---|
| Keep option labels short, with detail in `description`. | Put a paragraph in the label. |
| Order options logically (most common first, or a natural order). | Order them at random. |

## API (from the source)

The component's exact props, model, events, slots and types, with their documentation comments.

```ts
// RadioGroup.vue
/**
 * One choice from a few (two to five) that people should see side by side,
 * e.g. "Mark as leavers" or "Keep on roll". For more options, or when the
 * choice is less important than the space it takes, use Select.
 */
export type RadioValue = string | number

export interface RadioOption {
  label: string
  value: RadioValue
  /** More detail, in smaller text under the label. */
  description?: string
  disabled?: boolean
}

export interface RadioGroupProps {
  /** The question, above the options. Also the group's accessible name. */
  label: string
  options: RadioOption[]
  /** Extra guidance about the choice. */
  helper?: string
  /** Show helper text under the label, or behind a `?` icon next to it. */
  helperPlacement?: 'below' | 'icon'
  /** Error message, e.g. "Choose what happens to Year 13". */
  errorText?: string
  /** Name for native form submission. */
  name?: string
  required?: boolean
  disabled?: boolean
  class?: HTMLAttributes['class']
}

/** The chosen option's value; undefined until one is chosen. */
const model = defineModel<RadioValue | undefined>()
```
