Skip to content

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

ClassDescription
.radio-groupBase radio group container
.radio-group--primaryPrimary group variant
.radio-group--secondarySecondary group variant
.radioIndividual radio item
.radio__controlRadio control
.radio__indicatorRadio indicator
.radio__contentRadio content wrapper
.radio--disabledDisabled radio state

Interactive States

StateSelector
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

PropTypeDefaultDescription
v-modelstringundefinedControlled selected value
valuestringundefinedReact-style controlled selected value
defaultValuestringundefinedInitial selected value
variant'primary' | 'secondary''primary'Visual variant
namestringundefinedForm field name
orientation'vertical' | 'horizontal''vertical'Group orientation
disabledbooleanundefinedDisable the group
isDisabledbooleanundefinedReact-style disabled alias
readonlybooleanundefinedPrevent changing the selected value
isReadOnlybooleanundefinedReact-style read-only alias
isInvalidbooleanundefinedInvalid state
requiredbooleanundefinedNative required state
isRequiredbooleanundefinedReact-style required alias

RadioGroup Events

EventPayloadDescription
update:modelValuestringEmitted when selection changes
update:valuestringReact-style value sync event
changestringEmitted when selection changes

Radio Props

PropTypeDefaultDescription
valuestringRequiredRadio value
disabledbooleanundefinedDisable the radio item
isDisabledbooleanundefinedReact-style disabled alias
isInvalidbooleanundefinedInvalid state override
controlClassstringundefinedClass merged onto .radio__control
indicatorClassstringundefinedClass merged onto .radio__indicator
contentClassstringundefinedClass merged onto .radio__content

Radio Slots

SlotPropsDescription
default{ checked, isSelected, isDisabled, isInvalid, isReadOnly }Radio content
indicator{ checked, isSelected, isDisabled, isInvalid, isReadOnly }Optional custom indicator

Released under the Apache-2.0 License.