# RadioGroup (/docs/radio-group)





`RadioGroup` is a single-choice control: a set of circular options, each with an inline label. The circles reuse the family's shared control surface, so they sit flush beside an [`Input`](/docs/input), and `label` / `description` / `error` / `required` are built in via [`Field`](/docs/field). It implements the kit's form-control contract — `name` submits, and `ref` / `onBlur` land on the group root — so it binds to react-hook-form, TanStack Form, or a plain `<form>` with no glue.

```tsx
import { RadioGroup } from '@42-central/ui-react/radio-group';

<RadioGroup
  label="Coalition"
  data={[
    { value: 'federation', label: 'The Federation' },
    { value: 'order', label: 'The Order' },
    { value: 'alliance', label: 'The Alliance' },
  ]}
/>
```

The `data` prop also accepts bare strings, normalized to `{ value, label }`:

```tsx
<RadioGroup label="Size" data={['Small', 'Medium', 'Large']} />
```

## Playground [#playground]

<story.Playground />

## Orientation [#orientation]

Options stack vertically by default. Set `orientation="horizontal"` to lay them out in a wrapping row.

<Preview align="block" code="&#x22;<RadioGroup\n  label=\&#x22;Coalition\&#x22;\n  orientation=\&#x22;horizontal\&#x22;\n  defaultValue=\&#x22;order\&#x22;\n  data={[\n    { value: 'federation', label: 'The Federation' },\n    { value: 'order', label: 'The Order' },\n    { value: 'alliance', label: 'The Alliance' },\n  ]}\n/>&#x22;">
  <RadioGroup
    label="Coalition"
    orientation="horizontal"
    defaultValue="order"
    data="[
    { value: 'federation', label: 'The Federation' },
    { value: 'order', label: 'The Order' },
    { value: 'alliance', label: 'The Alliance' },
  ]"
  />
</Preview>

## Sizes [#sizes]

Five sizes — `xs` through `xl` — matching the rest of the family. The circle and its dot scale together.

<Preview align="block" code="&#x22;<RadioGroup size=\&#x22;xs\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'xs' }]} />\n<RadioGroup size=\&#x22;sm\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'sm' }]} />\n<RadioGroup size=\&#x22;md\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'md' }]} />\n<RadioGroup size=\&#x22;lg\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'lg' }]} />\n<RadioGroup size=\&#x22;xl\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'xl' }]} />&#x22;">
  <div className="flex flex-col gap-3">
    <RadioGroup size="xs" defaultValue="a" data="[{ value: 'a', label: 'xs' }]" />

    <RadioGroup size="sm" defaultValue="a" data="[{ value: 'a', label: 'sm' }]" />

    <RadioGroup size="md" defaultValue="a" data="[{ value: 'a', label: 'md' }]" />

    <RadioGroup size="lg" defaultValue="a" data="[{ value: 'a', label: 'lg' }]" />

    <RadioGroup size="xl" defaultValue="a" data="[{ value: 'a', label: 'xl' }]" />
  </div>
</Preview>

## Variants [#variants]

Two `variant`s decide the selected circle's fill. `default` is a neutral, monochrome circle that stays palette-independent; `filled` tints the circle with `color`. The two share one inner dot.

<Preview align="block" code="&#x22;<RadioGroup\n  variant=\&#x22;default\&#x22;\n  defaultValue=\&#x22;a\&#x22;\n  data={[{ value: 'a', label: 'Default' }]}\n/>\n<RadioGroup\n  variant=\&#x22;filled\&#x22;\n  color=\&#x22;brand\&#x22;\n  defaultValue=\&#x22;a\&#x22;\n  data={[{ value: 'a', label: 'Filled' }]}\n/>&#x22;">
  <div className="flex flex-col gap-3">
    <RadioGroup variant="default" defaultValue="a" data="[{ value: 'a', label: 'Default' }]" />

    <RadioGroup variant="filled" color="brand" defaultValue="a" data="[{ value: 'a', label: 'Filled' }]" />
  </div>
</Preview>

## Colors [#colors]

`color&#x60; tints the selected circle in the &#x2A;*`filled`** variant — the `default` variant stays monochrome. The resting circle is palette-independent, and the focus ring is always the brand ring. Set `data-color` once on the root and every kit palette tints for free.

<Preview align="block" code="&#x22;<RadioGroup variant=\&#x22;filled\&#x22; color=\&#x22;brand\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'brand' }]} />\n<RadioGroup variant=\&#x22;filled\&#x22; color=\&#x22;green\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'green' }]} />\n<RadioGroup variant=\&#x22;filled\&#x22; color=\&#x22;blue\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'blue' }]} />\n<RadioGroup variant=\&#x22;filled\&#x22; color=\&#x22;purple\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'purple' }]} />\n<RadioGroup variant=\&#x22;filled\&#x22; color=\&#x22;red\&#x22; defaultValue=\&#x22;a\&#x22; data={[{ value: 'a', label: 'red' }]} />&#x22;">
  <div className="flex flex-wrap gap-4">
    <RadioGroup variant="filled" color="brand" defaultValue="a" data="[{ value: 'a', label: 'brand' }]" />

    <RadioGroup variant="filled" color="green" defaultValue="a" data="[{ value: 'a', label: 'green' }]" />

    <RadioGroup variant="filled" color="blue" defaultValue="a" data="[{ value: 'a', label: 'blue' }]" />

    <RadioGroup variant="filled" color="purple" defaultValue="a" data="[{ value: 'a', label: 'purple' }]" />

    <RadioGroup variant="filled" color="red" defaultValue="a" data="[{ value: 'a', label: 'red' }]" />
  </div>
</Preview>

## Disabled & per-option disabled [#disabled--per-option-disabled]

`disabled` dims the whole group and blocks interaction. A single option can also be disabled by setting `disabled` on its `data` entry.

Each option also accepts its own optional `testId`, defaulting to `` `item-${value}` `` when omitted, rendered on that option's own element — see the [Testing](/docs/testing) guide.

<Preview align="block" code="&#x22;<RadioGroup\n  label=\&#x22;Coalition\&#x22;\n  defaultValue=\&#x22;federation\&#x22;\n  data={[\n    { value: 'federation', label: 'The Federation' },\n    { value: 'order', label: 'The Order (full)', disabled: true },\n    { value: 'alliance', label: 'The Alliance' },\n  ]}\n/>\n\n<RadioGroup label=\&#x22;Coalition\&#x22; disabled defaultValue=\&#x22;federation\&#x22; data={[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]} />&#x22;">
  <div className="flex flex-col gap-5">
    <RadioGroup
      label="Coalition"
      defaultValue="federation"
      data="[
      { value: 'federation', label: 'The Federation' },
      { value: 'order', label: 'The Order (full)', disabled: true },
      { value: 'alliance', label: 'The Alliance' },
    ]"
    />

    <RadioGroup label="Coalition" defaultValue="federation" data="[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]" />
  </div>
</Preview>

## With description & error [#with-description--error]

`description` (helper text) and `error` sit below the group via the built-in [`Field`](/docs/field). Passing `error` flips the circles to the invalid (red) state; `required` adds the semantic flag and a red asterisk on the label.

<Preview align="block" code="&#x22;<RadioGroup\n  label=\&#x22;Coalition\&#x22;\n  description=\&#x22;You can change this at any time.\&#x22;\n  defaultValue=\&#x22;order\&#x22;\n  data={[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]}\n/>\n\n<RadioGroup label=\&#x22;Pick a coalition\&#x22; required data={[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]} />\n\n<RadioGroup label=\&#x22;Pick a coalition\&#x22; error=\&#x22;Please choose a coalition\&#x22; data={[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]} />&#x22;">
  <div className="flex flex-col gap-5">
    <RadioGroup label="Coalition" description="You can change this at any time." defaultValue="order" data="[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]" />

    <RadioGroup label="Pick a coalition" data="[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]" />

    <RadioGroup label="Pick a coalition" error="Please choose a coalition" data="[{ value: 'federation', label: 'The Federation' }, { value: 'order', label: 'The Order' }]" />
  </div>
</Preview>

## Controlled [#controlled]

Drive the selection from state. `onChange` hands you the next value (a `string`).

```tsx
'use client';
import { useState } from 'react';
import { RadioGroup } from '@42-central/ui-react/radio-group';

export function CoalitionPicker() {
  const [coalition, setCoalition] = useState('federation');
  return (
    <RadioGroup
      label="Coalition"
      value={coalition}
      onChange={setCoalition}
      data={[
        { value: 'federation', label: 'The Federation' },
        { value: 'order', label: 'The Order' },
      ]}
    />
  );
}
```

## Forms [#forms]

Each option renders a hidden native `<input type="radio">` sharing the group `name`, so a `name` wires the selection into native `<form>` submission and `FormData`.

```tsx
<form action={save}>
  <RadioGroup
    name="coalition"
    label="Coalition"
    data={[
      { value: 'federation', label: 'The Federation' },
      { value: 'order', label: 'The Order' },
    ]}
  />
</form>
```

## API [#api]

<TypeTable
  type="{
  value: { type: 'string | null', description: 'Controlled selected value.' },
  defaultValue: { type: 'string | null', description: 'Uncontrolled initial value.' },
  onChange: { type: '(value: string) => void', description: 'Fires with the next selected value.' },
  data: {
    type: '({ value: string; label: ReactNode; disabled?: boolean } | string)[]',
    description: 'The options. Bare strings are normalized to { value, label }.',
  },
  orientation: { type: '&#x22;horizontal&#x22; | &#x22;vertical&#x22;', default: '&#x22;vertical&#x22;', description: 'Stacking direction.' },
  label: { type: 'ReactNode', description: 'Field label above the group.' },
  description: { type: 'ReactNode', description: 'Field helper text below the group.' },
  error: { type: 'ReactNode', description: 'Field error; sets the invalid state.' },
  required: { type: 'boolean', default: 'false', description: 'Required flag + asterisk.' },
  invalid: { type: 'boolean', default: 'false', description: &#x22;Red invalid state, OR'd with error.&#x22; },
  disabled: { type: 'boolean', default: 'false', description: 'Disable the group.' },
  readOnly: { type: 'boolean', default: 'false', description: 'Keep contrast but reject changes.' },
  size: { type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;', default: '&#x22;md&#x22;', description: 'Circle + dot sizing.' },
  variant: { type: '&#x22;default&#x22; | &#x22;filled&#x22;', default: '&#x22;default&#x22;', description: 'Selected circle fill: monochrome (default) or palette-colored (filled).' },
  color: { type: 'Color', default: '&#x22;brand&#x22;', description: 'Selected-circle accent in the filled variant.' },
  name: { type: 'string', description: 'Name shared by the options (form submission).' },
  classNames: { type: '{ root?, item?, control?, label?, field? }', description: 'Per-part class overrides.' },
  testId: { type: 'string', description: 'data-testid on the group root. See the per-item testId note above for the options themselves.' },
}"
/>

## Accessibility [#accessibility]

* Full RadioGroup model from Ark: arrow keys move between options and select, `Space` selects the focused option, focus shows the ring, and the group / label / description / error associations are wired (the latter via the built-in `Field`).
* Each option is backed by a hidden native `<input type="radio">` sharing the group `name`, exposing `role="radio"` with `aria-checked`.
* `invalid` (or `error`) sets `aria-invalid` on the options.
* RTL is honored via the document `dir` — circles and labels mirror automatically.
