Radio Group
<input type="radio"> + Headless UI RadioGroup
Allows selecting exactly one option from a set. Three visual styles: card picker, pill row, and classic.
Overview
Preview
Plan
Period
Selected: Monthly
Card picker (most expressive)
Preview
html
<fieldset class="space-y-2">
<legend class="sr-only">Choose a plan</legend>
<!-- Selected option -->
<label class="flex cursor-pointer items-start gap-3 rounded-lg border border-[#D40C37]/50 bg-[#D40C37]/5 p-4">
<input type="radio" name="plan" value="pro" class="sr-only" checked />
<div class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 border-[#D40C37]">
<div class="h-1.5 w-1.5 rounded-full bg-[#D40C37]"></div>
</div>
<div>
<p class="text-sm font-semibold text-zinc-100">Pro — $9/mo</p>
<p class="text-xs text-zinc-500 mt-0.5">Unlimited + API access</p>
</div>
</label>
<!-- Unselected option -->
<label class="flex cursor-pointer items-start gap-3 rounded-lg border border-zinc-800 p-4 hover:border-zinc-700 transition-colors">
<input type="radio" name="plan" value="team" class="sr-only" />
<div class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 border-zinc-600"></div>
<div>
<p class="text-sm font-semibold text-zinc-300">Team — $29/mo</p>
<p class="text-xs text-zinc-500 mt-0.5">Up to 10 seats</p>
</div>
</label>
</fieldset>Pill row
Preview
html
<div class="flex gap-1 rounded-lg bg-zinc-900 p-1">
<label class="relative cursor-pointer rounded-md bg-zinc-700 px-4 py-1.5 text-sm font-medium text-zinc-100 shadow-sm">
<input type="radio" name="period" value="monthly" class="sr-only" checked />
Monthly
</label>
<label class="relative cursor-pointer rounded-md px-4 py-1.5 text-sm font-medium text-zinc-500 hover:text-zinc-300 transition-colors">
<input type="radio" name="period" value="yearly" class="sr-only" />
Yearly
</label>
</div>Classic radio inputs
Preview
html
<fieldset class="space-y-3">
<legend class="sr-only">Notification preference</legend>
<label class="flex cursor-pointer items-center gap-3">
<input type="radio" name="notify" value="all" class="h-4 w-4 accent-[#D40C37] cursor-pointer" />
<div>
<p class="text-sm text-zinc-200">All notifications</p>
<p class="text-xs text-zinc-500">Get notified about all activity.</p>
</div>
</label>
<!-- repeat -->
</fieldset>With Vue reactivity
vue
<script setup>
import { ref } from 'vue'
const selected = ref('pro')
const plans = [
{ value: 'free', label: 'Free', desc: '100 downloads / day' },
{ value: 'pro', label: 'Pro — $9/mo', desc: 'Unlimited + API' },
]
</script>
<template>
<div class="space-y-2">
<label v-for="plan in plans" :key="plan.value"
class="flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition-all"
:class="selected === plan.value ? 'border-[#D40C37]/50 bg-[#D40C37]/5' : 'border-zinc-800 hover:border-zinc-700'"
>
<input type="radio" :value="plan.value" v-model="selected" class="sr-only" />
<div class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 transition-all"
:class="selected === plan.value ? 'border-[#D40C37]' : 'border-zinc-600'"
>
<div v-if="selected === plan.value" class="h-1.5 w-1.5 rounded-full bg-[#D40C37]"></div>
</div>
<div>
<p class="text-sm font-semibold text-zinc-100">{{ plan.label }}</p>
<p class="text-xs text-zinc-500 mt-0.5">{{ plan.desc }}</p>
</div>
</label>
</div>
</template>With Headless UI RadioGroup
vue
<script setup>
import { RadioGroup, RadioGroupLabel, RadioGroupOption } from '@headlessui/vue'
import { ref } from 'vue'
const selected = ref('pro')
</script>
<template>
<RadioGroup v-model="selected" class="space-y-2">
<RadioGroupLabel class="sr-only">Choose a plan</RadioGroupLabel>
<RadioGroupOption v-for="plan in plans" :key="plan.value" :value="plan.value" v-slot="{ checked }">
<div :class="['flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition-all focus:outline-none',
checked ? 'border-[#D40C37]/50 bg-[#D40C37]/5' : 'border-zinc-800 hover:border-zinc-700']">
<div :class="['mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2',
checked ? 'border-[#D40C37]' : 'border-zinc-600']">
<div v-if="checked" class="h-1.5 w-1.5 rounded-full bg-[#D40C37]"></div>
</div>
<div>
<p class="text-sm font-semibold text-zinc-100">{{ plan.label }}</p>
<p class="text-xs text-zinc-500">{{ plan.desc }}</p>
</div>
</div>
</RadioGroupOption>
</RadioGroup>
</template>Accessibility notes
- Always wrap radio inputs in a
<fieldset>with a<legend>(usesr-onlyif visual label isn't needed) - Native radio inputs handle
↑↓navigation within a group automatically - Headless UI's
RadioGroupadditionally providesaria-checkedon each option and manages rovingtabindex - Use
class="sr-only"on hidden inputs — neverdisplay:none— so they remain keyboard accessible