---
name: radio-group
type: core
domain: forms
requires: [loke-ui]
description: >
  RadioGroup + RadioGroupItem + RadioGroupIndicator for single-selection groups.
  Roving focus, arrow-key navigation with auto-selection, Enter prevention per WAI-ARIA.
  Hidden native radio input for form participation. Standalone Radio primitive available.
  value prop required on every item.
---

# Radio Group

`@loke/ui/radio-group` — single-selection group built on `Primitive.div` + `RovingFocusGroup`. Each item wraps the standalone `Radio` primitive with a hidden native `<input type="radio">`.

**Exports:** `RadioGroup`, `RadioGroupItem`, `RadioGroupIndicator`, `createRadioGroupScope`

## Setup

```tsx
import {
  RadioGroup,
  RadioGroupItem,
  RadioGroupIndicator,
} from "@loke/ui/radio-group";
import { Label } from "@loke/ui/label";

function PaymentMethod() {
  return (
    <RadioGroup defaultValue="card" name="payment">
      {[
        { value: "card", label: "Credit card" },
        { value: "paypal", label: "PayPal" },
        { value: "bank", label: "Bank transfer" },
      ].map(({ value, label }) => (
        <div key={value} style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <RadioGroupItem id={value} value={value}>
            <RadioGroupIndicator />
          </RadioGroupItem>
          <Label htmlFor={value}>{label}</Label>
        </div>
      ))}
    </RadioGroup>
  );
}
```

`data-state` on `RadioGroupItem` and `RadioGroupIndicator`: `"checked"` | `"unchecked"`

## Core Patterns

### Controlled value

```tsx
const [value, setValue] = useState("card");

<RadioGroup value={value} onValueChange={setValue} name="payment">
  <RadioGroupItem value="card"><RadioGroupIndicator /></RadioGroupItem>
  <RadioGroupItem value="paypal"><RadioGroupIndicator /></RadioGroupItem>
</RadioGroup>
```

### Orientation and RTL

`orientation` controls both the ARIA attribute and arrow-key direction. With `dir="rtl"`, left/right keys are mirrored automatically via `useDirection`.

```tsx
<RadioGroup orientation="horizontal" dir="rtl" defaultValue="a">
  <RadioGroupItem value="a"><RadioGroupIndicator /></RadioGroupItem>
  <RadioGroupItem value="b"><RadioGroupIndicator /></RadioGroupItem>
</RadioGroup>
```

`orientation` values: `"vertical"` (default) | `"horizontal"`

### Standalone Radio

Use `Radio` and `RadioIndicator` directly from `@loke/ui/radio-group` when you need a single radio outside a group context (e.g., inside a custom compound component).

```tsx
import { Radio, RadioIndicator } from "@loke/ui/radio-group";

// Radio manages its own checked/onCheck props
<Radio
  checked={isSelected}
  onCheck={() => setSelected(true)}
  name="solo"
  value="option-a"
>
  <RadioIndicator />
</Radio>
```

Note: `Radio` is exported from the `radio-group` subpath, not a separate subpath.

## Common Mistakes

### 1. Using Enter to select — it is intentionally blocked

Enter key is prevented on `RadioGroupItem` per the WAI-ARIA radio group pattern. Arrow keys navigate *and* select simultaneously via the roving focus mechanism.

**Wrong:** Adding an `onKeyDown` handler that triggers selection on `"Enter"`.

**Correct:** Arrow keys are the only keyboard selection mechanism. Do not add Enter-key selection logic.

Source: `src/components/radio-group/radio-group.tsx` — `if (event.key === "Enter") event.preventDefault()`

### 2. Expecting to uncheck a selected item

Radio buttons cannot be unchecked within a group once selected. `onCheck` on `Radio` only fires when `checked` is `false` — clicking an already-checked item does nothing.

**Wrong:** Adding toggle logic expecting `onValueChange` to receive `undefined` when re-clicking the current value.

**Correct:** Selection can only move to a different item. For optional selection, use a Checkbox or add a separate "Clear" control that resets state programmatically via `setValue(undefined)` on the controlled state.

Source: `src/components/radio-group/radio.tsx` — `if (!checked) onCheck?.()`

### 3. Missing value prop on RadioGroupItem

`RadioGroupItem` requires a `value` prop to identify itself within the group. Without it, the group's selection tracking breaks and no native `<input>` value is submitted.

**Wrong:**

```tsx
<RadioGroupItem>
  <RadioGroupIndicator />
</RadioGroupItem>
```

**Correct:**

```tsx
<RadioGroupItem value="option-a">
  <RadioGroupIndicator />
</RadioGroupItem>
```

Source: `src/components/radio-group/radio-group.tsx` — `context.value === itemProps.value` comparison

## Cross-references

- **Label** (`@loke/ui/label`) — associate labels with each `RadioGroupItem` via `htmlFor`
- **Checkbox** (`@loke/ui/checkbox`) — for multi-select or indeterminate scenarios
- **Choosing the Right Component** — RadioGroup vs Checkbox decision guide
