RadioGroupNew
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 value="basic">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Basic Plan</Label>
          <Description>Includes 100 messages per month</Description>
        </Radio.Content>
      </Radio>
      <Radio value="premium">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Premium Plan</Label>
          <Description>Includes 200 messages per month</Description>
        </Radio.Content>
      </Radio>
      <Radio value="business">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Business Plan</Label>
          <Description>Unlimited messages</Description>
        </Radio.Content>
      </Radio>
    </RadioGroup>
  );
}Anatomy
Import all parts and piece them together.
import {RadioGroup, Radio, Label, Description, FieldError} from '@heroui/react';
export default () => (
  <RadioGroup>
    <Label />
    <Description />
    <Radio value="option1">
      <Radio.Control>
        <Radio.Indicator>
          <span>✓</span> {/* Custom indicator (optional) */}
        </Radio.Indicator>
      </Radio.Control>
      <Radio.Content>
        <Label />
        <Description />
      </Radio.Content>
    </Radio>
    <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 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>
      <Radio 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>
      <Radio 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>
    </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 value="starter">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Starter</Label>
            <Description>For side projects</Description>
          </Radio.Content>
        </Radio>
        <Radio value="pro">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Pro</Label>
            <Description>Advanced reporting</Description>
          </Radio.Content>
        </Radio>
        <Radio value="teams">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Teams</Label>
            <Description>Up to 10 teammates</Description>
          </Radio.Content>
        </Radio>
      </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 value="starter">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Starter</Label>
            <Description>For side projects and small teams</Description>
          </Radio.Content>
        </Radio>
        <Radio value="pro">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Pro</Label>
            <Description>Advanced reporting and analytics</Description>
          </Radio.Content>
        </Radio>
        <Radio 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>
      </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 value="starter">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Starter</Label>
            <Description>For side projects and small teams</Description>
          </Radio.Content>
        </Radio>
        <Radio value="pro">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Pro</Label>
            <Description>Advanced reporting and analytics</Description>
          </Radio.Content>
        </Radio>
        <Radio 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>
      </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 value="starter">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Starter</Label>
            <Description>For side projects and small teams</Description>
          </Radio.Content>
        </Radio>
        <Radio value="pro">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Radio.Content>
            <Label>Pro</Label>
            <Description>Advanced reporting and analytics</Description>
          </Radio.Content>
        </Radio>
        <Radio 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>
        <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 value="starter">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Starter</Label>
          <Description>For side projects and small teams</Description>
        </Radio.Content>
      </Radio>
      <Radio value="pro">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Pro</Label>
          <Description>Advanced reporting and analytics</Description>
        </Radio.Content>
      </Radio>
      <Radio 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>
    </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
                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>
            ))}
          </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
                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>
            ))}
          </div>
        </RadioGroup>
      </section>
    </div>
  );
}Styling
Passing Tailwind CSS classes
import {RadioGroup, Radio, Label, Description} from '@heroui/react';
export default () => (
  <RadioGroup defaultValue="premium" name="plan">
    <Label>Plan selection</Label>
    <Description>Choose the plan that best suits your needs</Description>
    <Radio
      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.Control className="border-border border-2 group-hover:border-blue-400 group-data-[selected=true]:border-blue-500 group-data-[selected=true]:bg-blue-500">
        <Radio.Indicator />
      </Radio.Control>
      <Radio.Content>
        <Label className="cursor-pointer font-semibold">Basic Plan</Label>
        <Description className="text-sm">
          Includes 100 messages per month and up to 3 themes to set up
        </Description>
      </Radio.Content>
    </Radio>
    <Radio
      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.Control className="border-border border-2 group-hover:border-purple-400 group-data-[selected=true]:border-purple-500 group-data-[selected=true]:bg-purple-500">
        <Radio.Indicator />
      </Radio.Control>
      <Radio.Content>
        <Label className="cursor-pointer font-semibold">Premium Plan</Label>
        <Description className="text-sm">
          Includes 200 messages per month and up to 6 themes to set up
        </Description>
      </Radio.Content>
    </Radio>
    <Radio
      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.Control className="border-border border-2 group-hover:border-emerald-400 group-data-[selected=true]:border-emerald-500 group-data-[selected=true]:bg-emerald-500">
        <Radio.Indicator />
      </Radio.Control>
      <Radio.Content>
        <Label className="cursor-pointer font-semibold">Business Plan</Label>
        <Description className="text-sm">Unlimited messages and themes</Description>
      </Radio.Content>
    </Radio>
  </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 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 | - | Content to render inside the control, 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 | - | Content displayed next to the control, usually 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 |