---
title: RadioGroup
description: Choose one value from a visible set of radio options.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# RadioGroup

> Choose one value from a visible set of radio options.

Use RadioGroup for one choice from a small visible set. For a longer or space-constrained collection, use [Select](https://moraine.subf.dev/components/select.md).

## Basic usage

```tsx
import { Field, RadioGroup } from 'moraine'

export function Example() {
  return (
    <Field label="Visibility">
      <RadioGroup
        items={[
          { value: 'public', label: 'Public' },
          { value: 'private', label: 'Private' },
        ]}
      />
    </Field>
  )
}
```

## Anatomy

```text
RadioGroup [component; slot=root]
└── item [slot]
    ├── container [slot]
    │   ├── input [internal]
    │   └── control [slot]
    │       └── indicator [slot]
    └── wrapper [slot]
        ├── label [slot]
        └── description [slot]
```

## Usage

### Single selection

Use one string value for the selected item and pair `value` with `onValueChange` for controlled state. Disabled items remain visible but cannot be selected. The group uses radio keyboard behavior rather than the independent focus order of checkboxes.

```tsx
import { RadioGroup } from 'moraine'
import { createSignal } from 'solid-js'

const PLAN_OPTIONS = [
  { label: 'Free tier', value: 'free', description: 'For personal projects with basic features.' },
  {
    label: 'Pro tier',
    value: 'pro',
    description: 'Advanced collaboration and unlimited bandwidth.',
  },
  { label: 'Enterprise', value: 'enterprise', description: 'Dedicated support and custom SLA.' },
]

export function Selection() {
  const [plan, setPlan] = createSignal('pro')

  return (
    <div class="max-w-md w-full space-y-3">
      <RadioGroup items={PLAN_OPTIONS} value={plan()} onValueChange={setPlan} />
      <p class="text-xs text-muted-foreground">
        Active plan: <span class="text-foreground font-medium">{plan()}</span>
      </p>
    </div>
  )
}
```

### Forms

Set `name` for native form submission. For schema binding and validation, place the group inside
`form.Field` from [createForm](https://moraine.subf.dev/components/form.md). Standalone [Field](https://moraine.subf.dev/components/field.md) supplies
labels and messages without binding a form store.

```tsx
import { RadioGroup } from 'moraine'

const ENV_OPTIONS = [
  { label: 'Development', value: 'dev' },
  { label: 'Staging', value: 'staging' },
  { label: 'Production', value: 'prod' },
]

export function FormUsage() {
  return (
    <div class="max-w-md w-full">
      <RadioGroup name="environment" items={ENV_OPTIONS} defaultValue="staging" />
    </div>
  )
}
```

### Keyboard interaction

| Key                                          | Description                                                   |
| -------------------------------------------- | ------------------------------------------------------------- |
| <kbd>ArrowDown</kbd> / <kbd>ArrowRight</kbd> | Moves focus and selection to the next enabled radio item.     |
| <kbd>ArrowUp</kbd> / <kbd>ArrowLeft</kbd>    | Moves focus and selection to the previous enabled radio item. |
| <kbd>Space</kbd>                             | Selects the focused radio item.                               |
| <kbd>Tab</kbd>                               | Enters or leaves the group according to normal focus order.   |

## Examples

### Controlled value and disabled items

```tsx
import { RadioGroup } from 'moraine'
import { createSignal } from 'solid-js'

export function ControlledDisabled() {
  const [value, setValue] = createSignal('pro')

  return (
    <div class="max-w-xl space-y-3">
      <RadioGroup
        items={[
          {
            value: 'starter',
            label: 'Starter (Legacy)',
            description: 'For personal projects',
            disabled: true,
          },
          { value: 'pro', label: 'Pro', description: 'For teams and scaling' },
          { value: 'enterprise', label: 'Enterprise', description: 'For regulated workloads' },
        ]}
        value={value()}
        onValueChange={setValue}
        variant="table"
        orientation="horizontal"
      />
      <p class="text-xs text-muted-foreground">Current plan: {value()}</p>
    </div>
  )
}
```

### Indicator position

```tsx
import { RadioGroup } from 'moraine'
import type { RadioGroupT } from 'moraine'
import { For } from 'solid-js'

export function IndicatorPositions() {
  const ITEMS = [
    { value: 'starter', label: 'Starter', description: 'For personal projects' },
    { value: 'pro', label: 'Pro', description: 'For teams and scaling' },
    { value: 'enterprise', label: 'Enterprise', description: 'For regulated workloads' },
  ]

  const INDICATORS: RadioGroupIndicatorName[] = ['start', 'end', 'hidden']

  type RadioGroupIndicatorName = Exclude<RadioGroupT.Variant['indicator'], undefined>

  return (
    <div class="gap-4 grid lg:grid-cols-3 sm:grid-cols-2">
      <For each={INDICATORS}>
        {(indicator) => (
          <div class="p-4 b-1 b-border rounded-lg space-y-2">
            <p class="text-sm font-medium">Indicator: {indicator}</p>
            <RadioGroup items={ITEMS} variant="card" indicator={indicator} defaultValue="pro" />
          </div>
        )}
      </For>
    </div>
  )
}
```

### Form integration

```tsx
import { Button, createForm, RadioGroup } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

const ENV_OPTIONS = [
  { label: 'Development', value: 'dev' },
  { label: 'Staging', value: 'staging' },
  { label: 'Production', value: 'prod' },
]

export function FormIntegration() {
  const [submittedEnv, setSubmittedEnv] = createSignal('staging')
  const form = createForm({
    schema: v.object({
      environment: v.pipe(v.string(), v.nonEmpty('Please select a target environment.')),
    }),
    initialInput: { environment: 'staging' },
    validate: 'input',
  })

  return (
    <form.Form onSubmit={(output) => setSubmittedEnv(output.environment)}>
      <div class="max-w-xl space-y-4">
        <form.Field
          name="environment"
          label="Target Deployment Environment"
          description="Determines runtime configuration and secrets applied."
          required
        >
          <RadioGroup items={ENV_OPTIONS} />
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">Target: {submittedEnv()}</p>
        </div>
      </div>
    </form.Form>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `radio-group`, `radio-group-item`, `radio-group-control` | Present when the component, slot, or item is disabled. |
| `data-invalid` | `radio-group`, `radio-group-control` | Present when the field or form has a validation error. |
| `data-readonly` | `radio-group`, `radio-group-control` | Present when the field is in read-only mode. |
| `data-required` | `radio-group`, `radio-group-control` | Present when the field input is required. |
| `data-checked` | `radio-group-item`, `radio-group-control`, `radio-group-indicator` | Present when the item or toggle is checked. |

## Props

Props for the RadioGroup component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| defaultValue | string \| undefined | — | The default value of the input (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| id | string \| undefined | — | The ID of the input element. |
| indicator | 'start' \| 'end' \| 'hidden' \| undefined | 'start' | Placement of the selection indicator. |
| items | (string \| {<br>  /** Value of the radio item. */<br>  value?: string;<br>  /** Label for the radio item. */<br>  label?: JSX.Element;<br>  /** Description for the radio item. */<br>  description?: JSX.Element;<br>  /** Whether the item is disabled. */<br>  disabled?: boolean;<br>})[] \| undefined | — | Array of items to render in the group. |
| loop | boolean \| undefined | true | Whether keyboard navigation loops around when reaching the ends. |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onValueChange | ((value: string) => void) \| undefined | — | Callback when the selected value changes. |
| orientation | 'horizontal' \| 'vertical' \| undefined | 'vertical' | The orientation of the radio group. |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| required | boolean \| undefined | false | Whether the input is required. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| value | string \| undefined | — | The current value of the input (controlled). |
| variant | 'card' \| 'table' \| 'list' \| undefined | 'list' | Visual treatment of the component. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
