# Radio Group

> A set of radios where people pick exactly one option, with built-in labels, tones, sizes, and arrow-key movement.

- Name: `radio-group`
- Status: beta
- Categories: forms
- File: the install writes `radio-group.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`, `class-variance-authority`. The install adds them.
- Gallery: https://arbokui.com/components/radio-group
- Registry item: https://arbokui.com/r/radio-group.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/radio-group
   ```

## Use it for

- Picking exactly one option from a short list, like a delivery speed or a plan.
- Two to six options that all fit on screen. People compare them at a glance.
- Pass each option's label as RadioGroupItem children. Clicking the text checks the radio.
- Name the group with aria-label, or aria-labelledby pointing at a visible heading or legend.
- Set tone and size once, on RadioGroup. Every item follows them.

## Avoid it for

- Picking any number of options. Use checkboxes.
- Options with a hint, a price, or a consequence. Use SelectionGroup with a SelectionCard for each.
- A setting that turns on or off. Use a switch.
- Long lists, more than about seven options. Use a select or a combobox.
- Starting with nothing checked when one answer is the safe default. Set defaultValue to it.

## Props

- `RadioGroup`: `RadioGroupProps`. Root. Holds the value. Takes value, defaultValue, onValueChange, name, required, readOnly, and disabled.
- `aria-label`: `string`. RadioGroup. Names the group. TypeScript requires this or aria-labelledby.
- `aria-labelledby`: `string`. RadioGroup. The id of visible text that names the group, like a heading or a fieldset legend.
- `tone`: `"neutral" | "brand" | "danger" | "success" | "warning"`. Default `"neutral"`. RadioGroup. Color of the checked radio, for every item.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. RadioGroup. Radio size and label text size, for every item. Density scales them.
- `value`: `string`. RadioGroup. The checked option, controlled. Use it with onValueChange.
- `defaultValue`: `string`. RadioGroup. The option checked at the start when you do not control it.
- `onValueChange`: `(value: string, details) => void`. RadioGroup. Called with the new value when someone picks an option.
- `name`: `string`. RadioGroup. Form field name. The checked value is sent with the form.
- `disabled`: `boolean`. Default `false`. RadioGroup or RadioGroupItem. Blocks picking. On the group it blocks every item.
- `readOnly`: `boolean`. Default `false`. RadioGroup. Shows the value, but no one can change it. Items stay focusable.
- `required`: `boolean`. Default `false`. RadioGroup. A form will not send until an option is picked.
- `aria-invalid`: `boolean`. RadioGroup. Turns the group to the danger tone. Set it when validation fails, and link the message with aria-describedby.
- `RadioGroupItem`: `RadioGroupItemProps`. Part. One option. Takes value, which is required, and disabled.
- `children`: `ReactNode`. RadioGroupItem. Visible label text. Without it, TypeScript requires aria-label, aria-labelledby, or an id that a label points at.
- `className`: `string`. RadioGroup: classes for the group, so className='flex-row flex-wrap' lays items in a row. RadioGroupItem: classes for the circle. Later classes win.

## Accessibility

- The group has role="radiogroup". Each item renders a span with role="radio" plus a hidden native radio, so the value is sent with the form.
- The group is one tab stop. Tab lands on the checked radio, or on the first one when none is checked.
- Arrow keys move the choice and check the radio they land on. They skip disabled items. Space checks the focused radio.
- TypeScript requires a name for the group, and children, aria-label, aria-labelledby, or an id for each item. It cannot check that a label points at the id.
- In server HTML, only aria-label and aria-labelledby name a radio. A label from children or an id is linked once JavaScript runs.
- The unchecked border uses --input and the focus ring uses --ring. The theme tests hold both to 3:1 against the page and cards.
- The dot uses the tone's foreground on its fill, which the theme tests hold to 4.5:1.
- Press scale and the dot's pop-in follow --motion-\* tokens. Reduced motion turns both off and keeps the fade.

## Examples

### Named by a visible heading

```tsx
<div className="grid gap-3">
  <p id="delivery-label" className="text-sm font-medium">
    Delivery speed
  </p>
  <RadioGroup aria-labelledby="delivery-label" name="delivery" defaultValue="standard" tone="brand">
    <RadioGroupItem value="standard">Standard, 3 to 5 days</RadioGroupItem>
    <RadioGroupItem value="express">Express, next day</RadioGroupItem>
    <RadioGroupItem value="pickup">Pick up in store</RadioGroupItem>
  </RadioGroup>
</div>
```

### A label with a second line

```tsx
<RadioGroup aria-label="Plan" defaultValue="team">
  <RadioGroupItem value="solo">
    <span className="block font-medium">Solo</span>
    <span className="block text-muted-foreground">One person, three projects.</span>
  </RadioGroupItem>
  <RadioGroupItem value="team">
    <span className="block font-medium">Team</span>
    <span className="block text-muted-foreground">Up to ten people, every project.</span>
  </RadioGroupItem>
</RadioGroup>
```

### In a row

```tsx
<RadioGroup aria-label="Size" defaultValue="m" className="flex-row flex-wrap gap-x-6">
  <RadioGroupItem value="s">Small</RadioGroupItem>
  <RadioGroupItem value="m">Medium</RadioGroupItem>
  <RadioGroupItem value="l">Large</RadioGroupItem>
</RadioGroup>
```

### Controlled

```tsx
"use client";
import { useState } from "react";

function Theme() {
  const [theme, setTheme] = useState("system");
  return (
    <RadioGroup aria-label="Theme" value={theme} onValueChange={(value) => setTheme(value as string)}>
      <RadioGroupItem value="light">Light</RadioGroupItem>
      <RadioGroupItem value="dark">Dark</RadioGroupItem>
      <RadioGroupItem value="system">Match my device</RadioGroupItem>
    </RadioGroup>
  );
}
```

### Required, in a form, with an error

```tsx
"use client";
import { useState } from "react";

function DeliverySpeed() {
  const [missing, setMissing] = useState(false);
  return (
    <form
      noValidate
      className="grid gap-2"
      onSubmit={(event) => {
        if (!new FormData(event.currentTarget).get("speed")) {
          event.preventDefault();
          setMissing(true);
        }
      }}
    >
      <p id="speed-label" className="text-sm font-medium">
        Delivery speed
      </p>
      <RadioGroup
        name="speed"
        required
        aria-labelledby="speed-label"
        aria-invalid={missing || undefined}
        aria-describedby={missing ? "speed-error" : undefined}
        onValueChange={() => setMissing(false)}
      >
        <RadioGroupItem value="standard">Standard</RadioGroupItem>
        <RadioGroupItem value="express">Express</RadioGroupItem>
      </RadioGroup>
      {missing && (
        <p id="speed-error" className="text-sm text-destructive-soft-foreground">
          Pick a delivery speed.
        </p>
      )}
      <button type="submit">Order</button>
    </form>
  );
}
```
