RadioGroupUpdated
Radio group for selecting a single option from a list
Import
import { RadioGroup, Radio } from '@heroui/react';Usage
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Basic() {
return (
<RadioGroup defaultValue="premium" name="plan">
<Label>Plan selection</Label>
<Description>Choose the plan that suits you best</Description>
<Radio.Root value="basic">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Basic Plan</Label>
<Description>Includes 100 messages per month</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="premium">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Premium Plan</Label>
<Description>Includes 200 messages per month</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="business">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Business Plan</Label>
<Description>Unlimited messages</Description>
</Radio.Content>
</Radio.Root>
</RadioGroup>
);
}Anatomy
Import the RadioGroup component and access all parts using dot notation.
import {RadioGroup, Radio, Label, Description, FieldError} from '@heroui/react';
export default () => (
<RadioGroup>
<Label />
<Description />
<Radio.Root value="option1">
<Radio.Control>
<Radio.Indicator>
<span>✓</span> {/* Custom indicator (optional) */}
</Radio.Indicator>
</Radio.Control>
<Radio.Content>
<Label />
<Description />
</Radio.Content>
</Radio.Root>
<FieldError />
</RadioGroup>
)Custom Indicator
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function CustomIndicator() {
return (
<RadioGroup defaultValue="premium" name="plan-custom-indicator">
<Label>Plan selection</Label>
<Description>Choose the plan that suits you best</Description>
<Radio.Root value="basic">
<Radio.Control>
<Radio.Indicator>
{({isSelected}) =>
isSelected ? <span className="text-background text-xs leading-none">✓</span> : null
}
</Radio.Indicator>
</Radio.Control>
<Radio.Content>
<Label>Basic Plan</Label>
<Description>Includes 100 messages per month</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="premium">
<Radio.Control>
<Radio.Indicator>
{({isSelected}) =>
isSelected ? <span className="text-background text-xs leading-none">✓</span> : null
}
</Radio.Indicator>
</Radio.Control>
<Radio.Content>
<Label>Premium Plan</Label>
<Description>Includes 200 messages per month</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="business">
<Radio.Control>
<Radio.Indicator>
{({isSelected}) =>
isSelected ? <span className="text-background text-xs leading-none">✓</span> : null
}
</Radio.Indicator>
</Radio.Control>
<Radio.Content>
<Label>Business Plan</Label>
<Description>Unlimited messages</Description>
</Radio.Content>
</Radio.Root>
</RadioGroup>
);
}Horizontal Orientation
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Horizontal() {
return (
<div className="flex flex-col gap-4">
<Label>Subscription plan</Label>
<RadioGroup defaultValue="pro" name="plan-orientation" orientation="horizontal">
<Radio.Root value="starter">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Starter</Label>
<Description>For side projects</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="pro">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Pro</Label>
<Description>Advanced reporting</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="teams">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Teams</Label>
<Description>Up to 10 teammates</Description>
</Radio.Content>
</Radio.Root>
</RadioGroup>
</div>
);
}Controlled
Selected plan: pro
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
import React from "react";
export function Controlled() {
const [value, setValue] = React.useState("pro");
return (
<div className="flex flex-col gap-4">
<RadioGroup name="plan-controlled" value={value} onChange={setValue}>
<Label>Subscription plan</Label>
<Radio.Root value="starter">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="pro">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="teams">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio.Content>
</Radio.Root>
</RadioGroup>
<p className="text-muted text-sm">
Selected plan: <span className="font-medium">{value}</span>
</p>
</div>
);
}Uncontrolled
Combine defaultValue with onChange when you only need to react to updates.
Last chosen plan: pro
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
import React from "react";
export function Uncontrolled() {
const [selection, setSelection] = React.useState("pro");
return (
<div className="flex flex-col gap-4">
<RadioGroup
defaultValue="pro"
name="plan-uncontrolled"
onChange={(nextValue) => setSelection(nextValue)}
>
<Label>Subscription plan</Label>
<Radio.Root value="starter">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="pro">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="teams">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio.Content>
</Radio.Root>
</RadioGroup>
<p className="text-muted text-sm">
Last chosen plan: <span className="font-medium">{selection}</span>
</p>
</div>
);
}Validation
"use client";
import {Button, Description, FieldError, Form, Label, Radio, RadioGroup} from "@heroui/react";
import React from "react";
export function Validation() {
const [message, setMessage] = React.useState<string | null>(null);
return (
<Form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const value = formData.get("plan-validation");
setMessage(`Your chosen plan is: ${value}`);
}}
>
<RadioGroup isRequired name="plan-validation">
<Label>Subscription plan</Label>
<Radio.Root value="starter">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="pro">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="teams">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio.Content>
</Radio.Root>
<FieldError>Choose a subscription before continuing.</FieldError>
</RadioGroup>
<Button className="mt-2 w-fit" type="submit">
Submit
</Button>
{!!message && <p className="text-muted text-sm">{message}</p>}
</Form>
);
}Disabled
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Disabled() {
return (
<RadioGroup isDisabled defaultValue="pro" name="plan-disabled">
<Label>Subscription plan</Label>
<Description>Plan changes are temporarily paused while we roll out updates.</Description>
<Radio.Root value="starter">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Starter</Label>
<Description>For side projects and small teams</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="pro">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Pro</Label>
<Description>Advanced reporting and analytics</Description>
</Radio.Content>
</Radio.Root>
<Radio.Root value="teams">
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Radio.Content>
<Label>Teams</Label>
<Description>Share access with up to 10 teammates</Description>
</Radio.Content>
</Radio.Root>
</RadioGroup>
);
}Delivery & Payment
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
import {Icon} from "@iconify/react";
import clsx from "clsx";
export function DeliveryAndPayment() {
const deliveryOptions = [
{
description: "4-10 business days",
price: "$5.00",
title: "Standard",
value: "standard",
},
{
description: "2-5 business days",
price: "$16.00",
title: "Express",
value: "express",
},
{
description: "1 business day",
price: "$25.00",
title: "Super Fast",
value: "super-fast",
},
];
const paymentOptions = [
{
description: "Exp. on 01/2026",
icon: "uim:master-card",
title: "**** 8304",
value: "mastercard",
},
{
description: "Exp. on 01/2026",
icon: "streamline-logos:visa-logo-solid",
title: "**** 0123",
value: "visa",
},
{
description: "Pay with PayPal",
icon: "ic:baseline-paypal",
title: "PayPal",
value: "paypal",
},
];
return (
<div
className="flex w-full flex-col items-center gap-10"
style={{
// @ts-expect-error - Overrides default variables
"--accent": "#006FEE",
"--accent-foreground": "#fff",
"--accent-hover": "#006FEE",
"--border-width": "2px",
"--border-width-field": "2px",
"--focus": "#006FEE",
}}
>
<section className="flex w-full max-w-lg flex-col gap-4">
<RadioGroup defaultValue="express" name="delivery">
<Label>Delivery method</Label>
<div className="grid gap-x-4 md:grid-cols-3">
{deliveryOptions.map((option) => (
<Radio.Root
key={option.value}
value={option.value}
className={clsx(
"bg-surface-2 data-[selected=true]:border-accent data-[selected=true]:bg-accent/10 group relative flex-col gap-4 rounded-md border border-transparent px-5 py-4 transition-all",
"data-[focus-visible=true]:border-accent data-[focus-visible=true]:bg-accent/10",
)}
>
<Radio.Control className="absolute right-4 top-3 size-5">
<Radio.Indicator />
</Radio.Control>
<Radio.Content className="flex flex-col gap-6">
<div className="flex flex-col gap-1">
<Label>{option.title}</Label>
<Description>{option.description}</Description>
</div>
<span className="text-sm font-semibold">{option.price}</span>
</Radio.Content>
</Radio.Root>
))}
</div>
</RadioGroup>
</section>
<section className="flex w-full max-w-lg flex-col gap-4">
<RadioGroup defaultValue="visa" name="payment">
<div className="flex flex-wrap items-center justify-between gap-4">
<Label>Payment method</Label>
</div>
<div className="grid gap-x-4 md:grid-cols-2">
{paymentOptions.map((option) => (
<Radio.Root
key={option.value}
value={option.value}
className={clsx(
"bg-surface-2 group relative flex-col gap-4 rounded-md border border-transparent px-5 py-4 transition-all",
"data-[selected=true]:border-accent data-[selected=true]:bg-accent/10",
)}
>
<Radio.Control className="absolute right-4 top-3 size-5">
<Radio.Indicator />
</Radio.Control>
<Radio.Content className="flex flex-row items-start justify-start gap-4">
<Icon className="size-6" icon={option.icon} />
<div className="flex flex-col gap-1">
<Label>{option.title}</Label>
<Description>{option.description}</Description>
</div>
</Radio.Content>
</Radio.Root>
))}
</div>
</RadioGroup>
</section>
</div>
);
}Styling
Passing Tailwind CSS classes
import { RadioGroup, Radio } from '@heroui/react';
export default () => (
<RadioGroup defaultValue="premium" name="plan">
<Radio.Root
className="border-border group cursor-pointer rounded-xl border-2 p-4 hover:border-blue-300 data-[selected=true]:border-blue-500 data-[selected=true]:bg-blue-500/10"
value="basic"
>
<Radio.Indicator className="border-border border-2 group-hover:border-blue-400 group-data-[selected=true]:border-blue-500 group-data-[selected=true]:bg-blue-500" />
Basic Plan
</Radio.Root>
<Radio.Root
className="border-border group cursor-pointer rounded-xl border-2 p-4 hover:border-purple-300 data-[selected=true]:border-purple-500 data-[selected=true]:bg-purple-500/10"
value="premium"
>
<Radio.Indicator className="border-border border-2 group-hover:border-purple-400 group-data-[selected=true]:border-purple-500 group-data-[selected=true]:bg-purple-500" />
Premium Plan
</Radio.Root>
<Radio.Root
className="border-border group cursor-pointer rounded-xl border-2 p-4 hover:border-emerald-300 data-[selected=true]:border-emerald-500 data-[selected=true]:bg-emerald-500/10"
value="business"
>
<Radio.Indicator className="border-border border-2 group-hover:border-emerald-400 group-data-[selected=true]:border-emerald-500 group-data-[selected=true]:bg-emerald-500" />
Business Plan
</Radio.Root>
</RadioGroup>
);Customizing the component classes
To customize the RadioGroup component classes, you can use the @layer components directive.
Learn more.
@layer components {
.radio-group {
@apply gap-2;
}
.radio {
@apply gap-4 rounded-lg border border-border p-3 hover:bg-surface-hovered;
}
.radio__control {
@apply border-2 border-primary;
}
.radio__indicator {
@apply bg-primary;
}
.radio__content {
@apply gap-1;
}
}HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The RadioGroup component uses these CSS classes (View source styles):
Base Classes
.radio-group- Base radio group container.radio- Individual radio item.radio__control- Radio control (circular button).radio__indicator- Radio indicator (inner dot).radio__content- Radio content wrapper
Modifier Classes
.radio--disabled- Disabled radio state
Interactive States
The radio supports both CSS pseudo-classes and data attributes for flexibility:
- Selected:
[aria-checked="true"]or[data-selected="true"](indicator appears) - Hover:
:hoveror[data-hovered="true"](border color changes) - Focus:
:focus-visibleor[data-focus-visible="true"](shows focus ring) - Pressed:
:activeor[data-pressed="true"](scale transform) - Disabled:
:disabledor[aria-disabled="true"](reduced opacity, no pointer events) - Invalid:
[data-invalid="true"]or[aria-invalid="true"](error border color)
API Reference
RadioGroup Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The current value (controlled) |
defaultValue | string | - | The default value (uncontrolled) |
onChange | (value: string) => void | - | Handler called when the value changes |
isDisabled | boolean | false | Whether the radio group is disabled |
isRequired | boolean | false | Whether the radio group is required |
isReadOnly | boolean | false | Whether the radio group is read only |
isInvalid | boolean | false | Whether the radio group is in an invalid state |
name | string | - | The name of the radio group, used when submitting an HTML form |
orientation | 'horizontal' | 'vertical' | 'vertical' | The orientation of the radio group |
children | React.ReactNode | (values: RadioGroupRenderProps) => React.ReactNode | - | Radio group content or render prop |
Radio.Root Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The value of the radio button |
isDisabled | boolean | false | Whether the radio button is disabled |
name | string | - | The name of the radio button, used when submitting an HTML form |
children | React.ReactNode | (values: RadioRenderProps) => React.ReactNode | - | Radio content or render prop |
Radio.Control Props
Extends React.HTMLAttributes<HTMLSpanElement>.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The content to render inside the control wrapper (typically Radio.Indicator) |
Radio.Indicator Props
Extends React.HTMLAttributes<HTMLSpanElement>.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | (values: RadioRenderProps) => React.ReactNode | - | Optional content or render prop that receives the current radio state. |
Radio.Content Props
Extends React.HTMLAttributes<HTMLDivElement>.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The content to render inside the content wrapper (typically Label and Description) |
RadioRenderProps
When using the render prop pattern, these values are provided:
| Prop | Type | Description |
|---|---|---|
isSelected | boolean | Whether the radio is currently selected |
isHovered | boolean | Whether the radio is hovered |
isPressed | boolean | Whether the radio is currently pressed |
isFocused | boolean | Whether the radio is focused |
isFocusVisible | boolean | Whether the radio is keyboard focused |
isDisabled | boolean | Whether the radio is disabled |
isReadOnly | boolean | Whether the radio is read only |
isInvalid | boolean | Whether the radio is in an invalid state |