RadioGroupUpdated

Radio group for selecting a single option from a list

Import

import { RadioGroup, Radio } from '@heroui/react';

Usage

Plan selectionChoose the plan that suits you best
"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

Plan selectionChoose the plan that suits you best
"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

Subscription plan

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.

Subscription plan

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

Subscription plan
"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

Subscription planPlan changes are temporarily paused while we roll out updates.
"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

Delivery method
Payment method
"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: :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 Props

PropTypeDefaultDescription
valuestring-The current value (controlled)
defaultValuestring-The default value (uncontrolled)
onChange(value: string) => void-Handler called when the value changes
isDisabledbooleanfalseWhether the radio group is disabled
isRequiredbooleanfalseWhether the radio group is required
isReadOnlybooleanfalseWhether the radio group is read only
isInvalidbooleanfalseWhether the radio group is in an invalid state
namestring-The name of the radio group, used when submitting an HTML form
orientation'horizontal' | 'vertical''vertical'The orientation of the radio group
childrenReact.ReactNode | (values: RadioGroupRenderProps) => React.ReactNode-Radio group content or render prop

Radio.Root Props

PropTypeDefaultDescription
valuestring-The value of the radio button
isDisabledbooleanfalseWhether the radio button is disabled
namestring-The name of the radio button, used when submitting an HTML form
childrenReact.ReactNode | (values: RadioRenderProps) => React.ReactNode-Radio content or render prop

Radio.Control Props

Extends React.HTMLAttributes<HTMLSpanElement>.

PropTypeDefaultDescription
childrenReact.ReactNode-The content to render inside the control wrapper (typically Radio.Indicator)

Radio.Indicator Props

Extends React.HTMLAttributes<HTMLSpanElement>.

PropTypeDefaultDescription
childrenReact.ReactNode | (values: RadioRenderProps) => React.ReactNode-Optional content or render prop that receives the current radio state.

Radio.Content Props

Extends React.HTMLAttributes<HTMLDivElement>.

PropTypeDefaultDescription
childrenReact.ReactNode-The content to render inside the content wrapper (typically Label and Description)

RadioRenderProps

When using the render prop pattern, these values are provided:

PropTypeDescription
isSelectedbooleanWhether the radio is currently selected
isHoveredbooleanWhether the radio is hovered
isPressedbooleanWhether the radio is currently pressed
isFocusedbooleanWhether the radio is focused
isFocusVisiblebooleanWhether the radio is keyboard focused
isDisabledbooleanWhether the radio is disabled
isReadOnlybooleanWhether the radio is read only
isInvalidbooleanWhether the radio is in an invalid state