Radio Group
Form ControlsLocal guide ↗
import { RadioGroup } from "bits-ui" Plan cards
The item itself is the card, so data-state can style the complete checked surface
Selected plan: team
<script lang="ts">
import { RadioGroup } from 'bits-ui';
import { Check } from '@lucide/svelte';
const plans = [
{ value: 'starter', name: 'Starter', price: '$12', detail: 'For personal prototypes' },
{ value: 'team', name: 'Team', price: '$32', detail: 'Shared projects and roles' },
{ value: 'scale', name: 'Scale', price: '$89', detail: 'Audit logs and SSO' }
];
let plan = $state('team');
</script>
<div class="grid gap-3">
<RadioGroup.Root bind:value={plan} name="billing-plan" required class="grid gap-3 sm:grid-cols-3">
{#each plans as item (item.value)}
<RadioGroup.Item
value={item.value}
aria-label={`${item.name}, ${item.price} per month`}
class="group relative rounded-xl border border-ink-700 bg-ink-900 p-4 text-left transition hover:border-ink-600 data-[state=checked]:border-brand-500 data-[state=checked]:bg-brand-500/15 data-[state=checked]:ring-1 data-[state=checked]:ring-brand-500"
>
<span class="flex items-start justify-between gap-2">
<span class="font-semibold text-ink-50">{item.name}</span>
<span class="flex size-5 items-center justify-center rounded-full border border-ink-600 text-ink-950 group-data-[state=checked]:border-brand-500 group-data-[state=checked]:bg-brand-500">
<Check class="size-3.5 opacity-0 group-data-[state=checked]:opacity-100" strokeWidth={3} />
</span>
</span>
<span class="mt-3 block text-xl font-semibold text-ink-50">{item.price}<span class="muted text-xs font-normal">/mo</span></span>
<span class="muted mt-1 block text-xs">{item.detail}</span>
</RadioGroup.Item>
{/each}
</RadioGroup.Root>
<p class="text-center text-xs text-brand-400">Selected plan: {plan}</p>
</div>
Keyboard orientation
Orientation changes both layout and the arrow keys used by roving focus
Use Left / Right arrows
Priority: Normal
<script lang="ts">
import { RadioGroup } from 'bits-ui';
const priorities = ['Low', 'Normal', 'High'];
let priority = $state('Normal');
let orientation = $state<'horizontal' | 'vertical'>('horizontal');
</script>
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-2">
<button class="btn btn-sm" class:btn-primary={orientation === 'horizontal'} onclick={() => (orientation = 'horizontal')}>Horizontal</button>
<button class="btn btn-sm" class:btn-primary={orientation === 'vertical'} onclick={() => (orientation = 'vertical')}>Vertical</button>
<span class="muted text-xs">
{orientation === 'horizontal' ? 'Use Left / Right arrows' : 'Use Up / Down arrows'}
</span>
</div>
<RadioGroup.Root
bind:value={priority}
{orientation}
loop
class={orientation === 'horizontal' ? 'flex gap-2' : 'grid max-w-xs gap-2'}
>
{#each priorities as item (item)}
<RadioGroup.Item
value={item}
class="rounded-lg border border-ink-700 bg-ink-900 px-4 py-2 text-sm text-ink-200 transition data-[state=checked]:border-brand-500 data-[state=checked]:bg-brand-500 data-[state=checked]:font-medium data-[state=checked]:text-ink-950"
>
{item}
</RadioGroup.Item>
{/each}
</RadioGroup.Root>
<p class="text-xs text-brand-400">Priority: {priority}</p>
</div>
Every RadioGroup.Item requires a unique string value. The root defaults to vertical orientation: vertical groups use Up/Down, horizontal groups use Left/Right, and loop controls whether arrow navigation wraps.
Setting name on the root renders the hidden input that submits the selected string. required is applied to that hidden input for native form validation. A readonly group remains focusable and navigable but cannot change value; a disabled group is non-focusable and non-interactive.