# RadioGroup

A set of mutually exclusive choices.

- Group: Forms
- Import: `import { RadioGroup } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/radio-group/
- Related: checkbox, select

## Anatomy

- **Group** (required) — A <div role="radiogroup"> stacking its options. Being a div is why the label above it names the group by being pointed AT — aria-labelledby, not htmlFor — and why the words do not click through.
- **Row** (required) — The <label> RadioGroupItem wraps around control and words. It is the click target — a bare 18px circle is below every pointer-target guideline — and it is the only source of the option’s accessible name.
- **Circle** (required) — The 18px control itself, its border turning --accent when chosen.
- **Dot** — The 10px --accent fill inside the circle, present only on the chosen option.

## Best practices

### Do

- Name the group. Inside a Field its label does it, through aria-labelledby; standing alone it needs its own aria-label, and without either the group is announced as three unlabelled radios.
- Set defaultValue or value. Selection follows focus here, so a group that starts empty commits an answer the moment anybody arrows into it — including a reader who was only passing through on the way to the next field.
- Add an explicit “None” or “Any” option when the answer is genuinely optional: there is no way back to nothing once a radio is chosen, neither by clicking it again nor from the keyboard.

### Don’t

- Do not hang an expensive effect on onValueChange. Every arrow press commits, so a group whose options fetch or navigate fires once per key on the way past the ones nobody wanted.
- Do not reach past RadioGroupItem to the Radix primitive or hand-roll the row: selection-follows-focus is implemented in this item’s own focus handler, not upstream, so a hand-rolled one moves the outline and selects nothing.
- Do not disable one option to mean “not available here”: the roving focus skips it entirely, so a keyboard reader never learns the option exists. Say why in the Field’s hint and leave the option out.

## Accessibility

- One tab stop for the whole group; the arrow keys move between options, per the ARIA radiogroup pattern.
- The label is inside the <label>, so the whole row is the click target.

## Keyboard

- Tab — Moves into the group, and out of it — the whole group is one stop.
- ↑ / ↓ / ← / → — Moves between options AND selects as it goes.

## RadioGroup

A set of mutually exclusive choices. Radix owns the roving tabindex, so the whole group is ONE tab stop and the arrow keys move between options — which is what the ARIA radiogroup pattern requires and what a stack of hand-rolled `<input type="radio">` wrappers usually gets wrong. Inside a `Field` the group takes its name from that label, by pointing back at it: the root is a `<div role="radiogroup">` and `<label for>` does not bind to one, so the words above it click through no more than a `<legend>` does. Standing alone, it still needs an `aria-label` of its own — an unnamed group is three unlabelled radios.

Also accepts: `ComponentProps<typeof RadioGroupPrimitive.Root>`.

## RadioGroupItem

One option, and its label, as a single click target. The `<label>` wraps both, so the whole row is clickable — a bare 18px circle is below every pointer-target guideline and is miserable on a phone. It is also what gives the control its accessible name: remove the wrapper and the radio has no name at all. Selection follows focus, which is the half of the pattern that makes a radiogroup usable from the keyboard: moving to an option chooses it, so nobody has to press an extra key to commit. That is implemented here rather than inherited, because the upstream primitive gates it on a flag cleared by `keyup` and loses the race against its own focus move — see ARROW_GRACE_MS. Re-selecting an already-selected option is a no-op, so this stays correct even where the upstream path does fire.

### Props

- `children` (required) — `ReactNode`. The visible label. Rendered inside the `<label>` that wraps the control.

Also accepts: `Omit<ComponentProps<typeof RadioGroupPrimitive.Item>, 'children'>`.

## Example — default

```tsx
import { RadioGroup, RadioGroupItem } from '@misoto22/folio'

<RadioGroup defaultValue="system" aria-label="Appearance">
  <RadioGroupItem value="light">Light</RadioGroupItem>
  <RadioGroupItem value="dark">Dark</RadioGroupItem>
  <RadioGroupItem value="system">Match the system</RadioGroupItem>
</RadioGroup>
```

## Example — options with detail

```tsx
import { RadioGroup, RadioGroupItem, Text } from '@misoto22/folio'

<RadioGroup defaultValue="studio" aria-label="Plan" className="max-w-sm">
  {PLANS.map((plan) => (
    <RadioGroupItem key={plan.value} value={plan.value} className="items-start">
      <span className="flex flex-col gap-0.5">
        <Text as="span" size="sm" tone="strong">
          {plan.name}
        </Text>
        <Text as="span" size="xs" tone="muted">
          {plan.detail}
        </Text>
      </span>
    </RadioGroupItem>
  ))}
</RadioGroup>
```

## Example — a way back

```tsx
import { RadioGroup, RadioGroupItem } from '@misoto22/folio'

<RadioGroup defaultValue="any" aria-label="Delivery window">
  <RadioGroupItem value="any">Any time</RadioGroupItem>
  <RadioGroupItem value="morning">Morning — 8am to 12pm</RadioGroupItem>
  <RadioGroupItem value="afternoon">Afternoon — 12pm to 5pm</RadioGroupItem>
  <RadioGroupItem value="evening">Evening — 5pm to 8pm</RadioGroupItem>
</RadioGroup>
```
