@loke/ui
Version:
146 lines (105 loc) • 4.79 kB
Markdown
---
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
`/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 `/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** (`/ui/label`) — associate labels with each `RadioGroupItem` via `htmlFor`
- **Checkbox** (`/ui/checkbox`) — for multi-select or indeterminate scenarios
- **Choosing the Right Component** — RadioGroup vs Checkbox decision guide