RadioGroup
Radio group for selecting a single option from a list.
Import
ts
import {
Button,
Description,
FieldError,
Label,
Radio,
RadioGroup,
Surface,
} from '@heroui-vue/vue'Usage
Basic
Choose the plan that suits you best
<template>
<RadioGroup class="demo-radio-standard" default-value="premium" name="plan">
<Label>Plan selection</Label>
<Description>Choose the plan that suits you best</Description>
<Radio value="basic">
<Label>Basic Plan</Label>
<Description>Includes 100 messages per month</Description>
</Radio>
<Radio value="premium">
<Label>Premium Plan</Label>
<Description>Includes 200 messages per month</Description>
</Radio>
<Radio value="business">
<Label>Business Plan</Label>
<Description>Unlimited messages</Description>
</Radio>
</RadioGroup>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Anatomy
vue
<template>
<RadioGroup>
<Label />
<Description />
<Radio value="option">
<template #indicator="{ isSelected }">
<span v-if="isSelected">...</span>
</template>
<Label />
<Description />
</Radio>
<FieldError />
</RadioGroup>
</template>Custom Indicator
Choose the plan that suits you best
<template>
<RadioGroup class="demo-radio-standard" default-value="premium" name="plan-custom-indicator">
<Label>Plan selection</Label>
<Description>Choose the plan that suits you best</Description>
<Radio v-for="plan in plans" :key="plan.value" :value="plan.value">
<template #indicator="{ isSelected }">
<svg
v-if="isSelected"
class="size-3 text-inherit"
style="color: var(--color-accent-foreground);"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
>
<path d="m20 6-11 11-5-5" />
</svg>
</template>
<Label>{{ plan.label }}</Label>
<Description>{{ plan.description }}</Description>
</Radio>
</RadioGroup>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
const plans = [
{ value: 'basic', label: 'Basic Plan', description: 'Includes 100 messages per month' },
{ value: 'premium', label: 'Premium Plan', description: 'Includes 200 messages per month' },
{ value: 'business', label: 'Business Plan', description: 'Unlimited messages' },
]
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Horizontal Orientation
<template>
<div class="flex flex-col gap-4">
<Label>Subscription plan</Label>
<RadioGroup class="demo-radio-standard" default-value="pro" name="plan-orientation" orientation="horizontal">
<Radio value="starter">
<Label>Starter</Label>
<Description>For side projects</Description>
</Radio>
<Radio value="pro">
<Label>Pro</Label>
<Description>Advanced reporting</Description>
</Radio>
<Radio value="teams">
<Label>Teams</Label>
<Description>Up to 10 teammates</Description>
</Radio>
</RadioGroup>
</div>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Controlled
Selected plan: pro
<template>
<div class="flex flex-col gap-4">
<RadioGroup v-model="value" class="demo-radio-standard" name="plan-controlled">
<Label>Subscription plan</Label>
<Radio value="starter">
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio>
<Radio value="pro">
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio>
<Radio value="teams">
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio>
</RadioGroup>
<p class="text-sm text-muted">
Selected plan: <span class="font-medium">{{ value }}</span>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
const value = ref('pro')
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Uncontrolled
Last chosen plan: pro
<template>
<div class="flex flex-col gap-4">
<RadioGroup class="demo-radio-standard" default-value="pro" name="plan-uncontrolled" @change="selection = $event">
<Label>Subscription plan</Label>
<Radio value="starter">
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio>
<Radio value="pro">
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio>
<Radio value="teams">
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio>
</RadioGroup>
<p class="text-sm text-muted">
Last chosen plan: <span class="font-medium">{{ selection }}</span>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
const selection = ref('pro')
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Validation
<template>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<RadioGroup v-model="plan" class="demo-radio-standard" is-required :is-invalid="submitted && !plan" name="plan-validation">
<Label>Subscription plan</Label>
<Radio value="starter">
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio>
<Radio value="pro">
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio>
<Radio value="teams">
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio>
<FieldError v-if="submitted && !plan">Choose a subscription before continuing.</FieldError>
</RadioGroup>
<Button class="w-fit" type="submit">Submit</Button>
<p v-if="message" class="text-sm text-muted">{{ message }}</p>
</form>
</template>
<script setup>
import { ref } from 'vue'
import { Button, Description, FieldError, Label, Radio, RadioGroup } from '@heroui-vue/vue'
const plan = ref('')
const submitted = ref(false)
const message = ref('')
const handleSubmit = () => {
submitted.value = true
message.value = plan.value ? `Your chosen plan is: ${plan.value}` : ''
}
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Disabled
Plan changes are temporarily paused while we roll out updates.
<template>
<RadioGroup class="demo-radio-standard" is-disabled default-value="pro" name="plan-disabled">
<Label>Subscription plan</Label>
<Description>Plan changes are temporarily paused while we roll out updates.</Description>
<Radio value="starter">
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio>
<Radio value="pro">
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio>
<Radio value="teams">
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio>
</RadioGroup>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Variants
RadioGroup supports two visual variants:
primary: default styling with field shadow.secondary: lower-emphasis styling for surfaces and dense layouts.
Primary variant
Secondary variant
<template>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-2">
<p class="text-sm font-medium text-muted">Primary variant</p>
<RadioGroup class="demo-radio-standard" default-value="option1" name="primary-plan" variant="primary">
<Radio value="option1">
<Label>Option 1</Label>
<Description>Standard styling with default background</Description>
</Radio>
<Radio value="option2">
<Label>Option 2</Label>
<Description>Another option with primary styling</Description>
</Radio>
</RadioGroup>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-medium text-muted">Secondary variant</p>
<RadioGroup class="demo-radio-standard" default-value="option1" name="secondary-plan" variant="secondary">
<Radio value="option1">
<Label>Option 1</Label>
<Description>Lower emphasis variant for use in surfaces</Description>
</Radio>
<Radio value="option2">
<Label>Option 2</Label>
<Description>Another option with secondary styling</Description>
</Radio>
</RadioGroup>
</div>
</div>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>In Surface
Use variant="secondary" when the group sits inside a Surface.
Choose the plan that suits you best
<template>
<Surface class="demo-radio-surface">
<RadioGroup class="demo-radio-standard" default-value="premium" name="plan-on-surface" variant="secondary">
<Label>Plan selection</Label>
<Description>Choose the plan that suits you best</Description>
<Radio value="basic">
<Label>Basic Plan</Label>
<Description>Includes 100 messages per month</Description>
</Radio>
<Radio value="premium">
<Label>Premium Plan</Label>
<Description>Includes 200 messages per month</Description>
</Radio>
<Radio value="business">
<Label>Business Plan</Label>
<Description>Unlimited messages</Description>
</Radio>
</RadioGroup>
</Surface>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup, Surface } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-radio-surface {
width: 100%;
max-width: 28rem;
padding: 1.5rem;
border: 1px solid color-mix(in oklch, var(--vp-c-divider), transparent 20%);
border-radius: 1.5rem;
background: var(--color-surface);
box-shadow:
0 1px 2px oklch(0% 0 0 / 5%),
0 16px 40px oklch(36% 0.05 260 / 8%);
}
.demo-radio-standard {
width: 24rem;
max-width: 100%;
}
</style>Delivery & Payment
<template>
<div class="demo-radio-checkout-shell">
<section class="demo-radio-section">
<RadioGroup default-value="express" name="delivery" variant="secondary">
<Label>Delivery method</Label>
<div class="demo-radio-card-grid">
<Radio
v-for="option in deliveryOptions"
:key="option.value"
class="demo-radio-card"
control-class="demo-radio-card__control"
content-class="demo-radio-card__content"
:value="option.value"
>
<div class="flex flex-col gap-1">
<Label>{{ option.title }}</Label>
<Description>{{ option.description }}</Description>
</div>
<span class="text-sm font-semibold">{{ option.price }}</span>
</Radio>
</div>
</RadioGroup>
</section>
<section class="demo-radio-section">
<RadioGroup default-value="visa" name="payment" variant="secondary">
<Label>Payment method</Label>
<div class="demo-radio-card-grid demo-radio-card-grid--payment">
<Radio
v-for="option in paymentOptions"
:key="option.value"
class="demo-radio-card"
control-class="demo-radio-card__control"
content-class="demo-radio-card__content demo-radio-card__content--row"
:value="option.value"
>
<span class="demo-payment-icon">{{ option.icon }}</span>
<div class="flex flex-col gap-1">
<Label>{{ option.title }}</Label>
<Description>{{ option.description }}</Description>
</div>
</Radio>
</div>
</RadioGroup>
</section>
</div>
</template>
<script setup>
import { Description, Label, Radio, RadioGroup } from '@heroui-vue/vue'
const deliveryOptions = [
{ value: 'standard', title: 'Standard', description: '4-10 business days', price: '$5.00' },
{ value: 'express', title: 'Express', description: '2-5 business days', price: '$16.00' },
{ value: 'super-fast', title: 'Super Fast', description: '1 business day', price: '$25.00' },
]
const paymentOptions = [
{ value: 'mastercard', icon: 'MC', title: '**** 8304', description: 'Exp. on 01/2026' },
{ value: 'visa', icon: 'VISA', title: '**** 0123', description: 'Exp. on 01/2026' },
{ value: 'paypal', icon: 'PP', title: 'PayPal', description: 'Pay with PayPal' },
]
</script>
<style lang="less">
.demo-radio-checkout-shell {
display: flex;
width: 100%;
max-width: 36rem;
flex-direction: column;
gap: 2.5rem;
align-items: stretch;
padding: 1.5rem;
border-radius: 1.5rem;
background: #f5f5f5;
text-align: left;
}
.demo-radio-section {
display: flex;
width: 100%;
flex-direction: column;
gap: 1rem;
}
.demo-radio-card-grid {
display: grid;
gap: 1rem;
width: 100%;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.demo-radio-card-grid--payment {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.demo-radio-card {
position: relative;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
min-width: 0;
width: 100%;
padding: 1rem 1.25rem;
border: 1px solid transparent;
border-radius: 0.75rem;
background: var(--color-surface);
text-align: left;
box-shadow: 0 1px 2px oklch(0% 0 0 / 4%);
transition:
border-color 160ms var(--ease-out),
background-color 160ms var(--ease-out),
box-shadow 160ms var(--ease-out);
}
.demo-radio-card[data-selected="true"] {
border-color: var(--color-accent);
background: color-mix(in oklch, var(--color-accent), transparent 90%);
}
.demo-radio-card__control {
position: absolute;
top: 0.75rem;
right: 1rem;
width: 1.25rem;
height: 1.25rem;
}
.demo-radio-card__content {
width: 100%;
gap: 1.5rem;
padding-right: 1.5rem;
text-align: left;
}
.demo-radio-card__content--row {
flex-direction: row;
align-items: flex-start;
gap: 1rem;
}
.demo-payment-icon {
display: inline-flex;
min-width: 2.25rem;
height: 1.5rem;
align-items: center;
justify-content: center;
border-radius: 0.375rem;
background: var(--color-default);
color: var(--color-default-foreground);
font-size: 0.7rem;
font-weight: 700;
}
@media (max-width: 720px) {
.demo-radio-card-grid,
.demo-radio-card-grid--payment {
grid-template-columns: 1fr;
}
}
</style>Styling
Passing Classes
vue
<template>
<RadioGroup default-value="premium" name="plan">
<Radio
class="rounded-xl border p-4 data-[selected=true]:border-accent"
value="basic"
>
Basic Plan
</Radio>
</RadioGroup>
</template>CSS Classes
| Class | Description |
|---|---|
.radio-group | Base radio group container |
.radio-group--primary | Primary group variant |
.radio-group--secondary | Secondary group variant |
.radio | Individual radio item |
.radio__control | Radio control |
.radio__indicator | Radio indicator |
.radio__content | Radio content wrapper |
.radio--disabled | Disabled radio state |
Interactive States
| State | Selector |
|---|---|
| Selected | [aria-checked="true"] / [data-selected="true"] |
| Hover | :hover / [data-hovered="true"] |
| Focus visible | :focus-visible / [data-focus-visible="true"] |
| Pressed | :active / [data-pressed="true"] |
| Disabled | [aria-disabled="true"] / [data-disabled="true"] |
| Read only | [aria-readonly="true"] / [data-readonly="true"] |
| Invalid | [aria-invalid="true"] / [data-invalid="true"] |
| Required | [data-required="true"] |
API
RadioGroup Props
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | string | undefined | Controlled selected value |
value | string | undefined | React-style controlled selected value |
defaultValue | string | undefined | Initial selected value |
variant | 'primary' | 'secondary' | 'primary' | Visual variant |
name | string | undefined | Form field name |
orientation | 'vertical' | 'horizontal' | 'vertical' | Group orientation |
disabled | boolean | undefined | Disable the group |
isDisabled | boolean | undefined | React-style disabled alias |
readonly | boolean | undefined | Prevent changing the selected value |
isReadOnly | boolean | undefined | React-style read-only alias |
isInvalid | boolean | undefined | Invalid state |
required | boolean | undefined | Native required state |
isRequired | boolean | undefined | React-style required alias |
RadioGroup Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Emitted when selection changes |
update:value | string | React-style value sync event |
change | string | Emitted when selection changes |
Radio Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | Required | Radio value |
disabled | boolean | undefined | Disable the radio item |
isDisabled | boolean | undefined | React-style disabled alias |
isInvalid | boolean | undefined | Invalid state override |
controlClass | string | undefined | Class merged onto .radio__control |
indicatorClass | string | undefined | Class merged onto .radio__indicator |
contentClass | string | undefined | Class merged onto .radio__content |
Radio Slots
| Slot | Props | Description |
|---|---|---|
default | { checked, isSelected, isDisabled, isInvalid, isReadOnly } | Radio content |
indicator | { checked, isSelected, isDisabled, isInvalid, isReadOnly } | Optional custom indicator |