# RadioGroup
**Category**: react
**URL**: https://heroui.com/en/docs/react/components/radio-group
**Source**: https://raw.githubusercontent.com/heroui-inc/heroui/refs/heads/v3/apps/docs/content/docs/en/react/components/(forms)/radio-group.mdx
> Radio group for selecting a single option from a list
***
## Usage
```tsx
import { RadioGroup, Radio } from '@heroui/react';
```
```tsx
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Basic() {
return (
Choose the plan that suits you best
Basic Plan
Includes 100 messages per month
Premium Plan
Includes 200 messages per month
Business Plan
Unlimited messages
);
}
```
## Anatomy
```tsx
import {RadioGroup, Radio, Label, Description, FieldError} from '@heroui/react';
export default () => (
{/* The clickable area: control + label */}
✓ {/* Custom indicator (optional) */}
Label {/* plain text — the clickable label */}
{/* Sibling — stays outside the button (announced via aria-describedby) */}
{/* Optional — per-radio validation message */}
{/* Optional — group-level validation */}
)
```
## Examples
### Horizontal Orientation
```tsx
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Horizontal() {
return (
Starter
For side projects
Pro
Advanced reporting
Teams
Up to 10 teammates
);
}
```
### Variants
The RadioGroup component supports two visual variants:
- **`primary`** (default) - Standard styling with default background, suitable for most use cases
- **`secondary`** - Lower emphasis variant, suitable for use in Surface components
```tsx
import {Description, Radio, RadioGroup} from "@heroui/react";
export function Variants() {
return (
Primary variant
Option 1
Standard styling with default background
Option 2
Another option with primary styling
Secondary variant
Option 1
Lower emphasis variant for use in surfaces
Option 2
Another option with secondary styling
);
}
```
### In Surface
When used inside a [Surface](/docs/components/surface) component, use `variant="secondary"` to apply the lower emphasis variant suitable for surface backgrounds.
```tsx
import {Description, Label, Radio, RadioGroup, Surface} from "@heroui/react";
export function OnSurface() {
return (
Choose the plan that suits you best
Basic Plan
Includes 100 messages per month
Premium Plan
Includes 200 messages per month
Business Plan
Unlimited messages
);
}
```
### Disabled
```tsx
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Disabled() {
return (
Plan changes are temporarily paused while we roll out updates.
Starter
For side projects and small teams
Pro
Advanced reporting and analytics
Teams
Share access with up to 10 teammates
);
}
```
### Controlled
```tsx
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
import React from "react";
export function Controlled() {
const [value, setValue] = React.useState("pro");
return (
Starter
For side projects and small teams
Pro
Advanced reporting and analytics
Teams
Share access with up to 10 teammates
Selected plan: {value}
);
}
```
### Uncontrolled
Combine `defaultValue` with `onChange` when you only need to react to updates.
```tsx
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
import React from "react";
export function Uncontrolled() {
const [selection, setSelection] = React.useState("pro");
return (
setSelection(nextValue)}
>
Starter
For side projects and small teams
Pro
Advanced reporting and analytics
Teams
Share access with up to 10 teammates
Last chosen plan: {selection}
);
}
```
### Validation
```tsx
"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(null);
return (
);
}
```
### Delivery & Payment
### Custom Indicator
```tsx
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function CustomIndicator() {
return (
Choose the plan that suits you best
{({isSelected}) =>
isSelected ? ✓ : null
}
Basic Plan
Includes 100 messages per month
{({isSelected}) =>
isSelected ? ✓ : null
}
Premium Plan
Includes 200 messages per month
{({isSelected}) =>
isSelected ? ✓ : null
}
Business Plan
Unlimited messages
);
}
```
### Render Function
```tsx
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function RenderFunction() {
return (
}
>
Choose the plan that suits you best
Basic Plan
Includes 100 messages per month
Premium Plan
Includes 200 messages per month
Business Plan
Unlimited messages
);
}
```
## Customization
### Tailwind CSS
```tsx
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
const options = [
{description: "$12 billed every month", label: "Monthly", value: "monthly"},
{description: "$120 billed once a year", label: "Yearly", value: "yearly"},
] as const;
const contentClassName =
"group flex w-full items-start gap-3 rounded-xl border border-success/10 bg-success-soft/30 px-4 py-3 transition-colors data-[focus-visible=true]:ring-2 data-[focus-visible=true]:ring-success/15 data-[hovered=true]:bg-success-soft-hover data-[selected=true]:border-success/30 data-[selected=true]:bg-success-soft data-[selected=true]:data-[hovered=true]:bg-success-soft-hover";
const controlClassName =
"mt-0.5 size-5 shrink-0 rounded-full border border-border bg-default shadow-none group-data-[pressed=true]:scale-95 group-data-[selected=true]:border-transparent group-data-[selected=true]:bg-success group-data-[selected=true]:group-data-[pressed=true]:bg-success-hover";
const indicatorClassName =
"before:rounded-full before:bg-default group-data-[selected=true]:before:scale-50 group-data-[selected=true]:before:bg-success-foreground group-data-[selected=true]:group-data-[pressed=true]:before:scale-[0.57]";
export function CustomStyles() {
return (
Choose how often you are charged.
{options.map(({description, label, value}) => (
{label}
{description}
))}
);
}
```
### Global CSS
To customize the RadioGroup component classes, you can use the `@layer components` directive.
[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
```css
@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-accent;
}
.radio__indicator {
@apply bg-primary;
}
.radio__content {
@apply gap-1;
}
}
```
## Styling Reference
HeroUI follows the [BEM](https://getbem.com/) 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](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/radio-group.css)):
#### Base Classes [!toc]
- `.radio-group` - Base radio group container
- `.radio` - Individual radio item
- `.radio__content` - Radio button (RAC label wrapper for control + label text)
- `.radio__control` - Radio control (circular button)
- `.radio__indicator` - Radio indicator (inner dot)
#### Modifier Classes [!toc]
- `.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**: `:hover` or `[data-hovered="true"]` (border color changes)
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]` (shows focus ring)
- **Pressed**: `:active` or `[data-pressed="true"]` (scale transform)
- **Disabled**: `:disabled` or `[aria-disabled="true"]` (reduced opacity, no pointer events)
- **Invalid**: `[data-invalid="true"]` or `[aria-invalid="true"]` (error border color)
## API Reference
### RadioGroup
| 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 |
| `variant` | `"primary" \| "secondary"` | `"primary"` | Visual variant of the component. `primary` is the default style with shadow. `secondary` is a lower emphasis variant without shadow, suitable for use in surfaces. |
| `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 |
| `render` | `DOMRenderFunction` | - | Overrides the default DOM element with a custom render function.|
### Radio
| 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: RadioFieldRenderProps) => React.ReactNode` | - | Radio content or field render prop |
| `render` | `DOMRenderFunction` | - | Overrides the default DOM element with a custom render function.|
### Radio.Control
Extends `React.HTMLAttributes`.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `React.ReactNode` | - | The content to render inside the control wrapper (typically Radio.Indicator) |
### Radio.Indicator
Extends `React.HTMLAttributes`.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `React.ReactNode \| (values: RadioButtonRenderProps) => React.ReactNode` | - | Optional content or render prop that receives the current radio button state. |
### Radio.Content
The clickable area of the radio (the `