Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Radio

A customizable radio group component with smooth animations and flexible layouts.

<RadioGroup defaultValue="option1">
  <RadioItem value="option1" label="Option 1" />
  <RadioItem value="option2" label="Option 2" />
  <RadioItem value="option3" label="Option 3" />
</RadioGroup>

Installation

Install the required dependencies:

npm install @radix-ui/react-radio-group motion
pnpm add @radix-ui/react-radio-group motion
yarn add @radix-ui/react-radio-group motion
bun add @radix-ui/react-radio-group motion

Copy and paste the following code into your project:

src/components/ui/Radio/radio.tsx
"use client";

import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "motion/react";

const radioGroupVariants = cva("grid gap-2", {
  variants: {
    orientation: {
      vertical: "grid-cols-1",
      horizontal: "grid-flow-col auto-cols-max",
    },
  },
  defaultVariants: {
    orientation: "vertical",
  },
});

const radioVariants = cva(
  "aspect-square rounded-full border border-[hsl(var(--hu-border))] text-[hsl(var(--hu-primary))] focus:outline-none focus-visible:ring-1 focus-visible:ring-[hsl(var(--hu-ring))] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-[hsl(var(--hu-primary))]",
  {
    variants: {
      size: {
        sm: "h-3 w-3",
        default: "h-4 w-4",
        lg: "h-5 w-5",
      },
    },
    defaultVariants: {
      size: "default",
    },
  }
);

interface RadioGroupProps
  extends Omit<
      React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
      "orientation"
    >,
    VariantProps<typeof radioGroupVariants> {
  label?: string;
  description?: string;
  error?: string;
}

interface RadioItemProps
  extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
    VariantProps<typeof radioVariants> {
  label?: string;
  description?: string;
}

const RadioGroup = React.forwardRef<
  React.ElementRef<typeof RadioGroupPrimitive.Root>,
  RadioGroupProps
>(
  (
    { className, orientation, label, description, error, id, ...props },
    ref
  ) => {
    const groupId = id || React.useId();

    return (
      <div className="flex flex-col gap-4">
        {(label || description) && (
          <div className="grid gap-1.5">
            {label && (
              <label
                htmlFor={groupId}
                className="text-sm font-medium leading-none"
              >
                {label}
              </label>
            )}
            {description && (
              <p className="text-xs text-[hsl(var(--hu-muted-foreground))]">
                {description}
              </p>
            )}
          </div>
        )}

        <RadioGroupPrimitive.Root
          ref={ref}
          id={groupId}
          className={cn(radioGroupVariants({ orientation }), className)}
          {...props}
        />

        {error && (
          <p className="text-xs text-[hsl(var(--hu-destructive))]">{error}</p>
        )}
      </div>
    );
  }
);

RadioGroup.displayName = "RadioGroup";

const RadioItem = React.forwardRef<
  React.ElementRef<typeof RadioGroupPrimitive.Item>,
  RadioItemProps
>(({ className, size, label, description, id, ...props }, ref) => {
  const itemId = id || React.useId();
  const dotSize = size === "sm" ? 5 : size === "lg" ? 8 : 6;

  return (
    <div className="flex flex-col gap-1">
      <div className="flex items-start gap-2">
        <RadioGroupPrimitive.Item
          ref={ref}
          id={itemId}
          className={cn(radioVariants({ size }), className)}
          {...props}
        >
          <RadioGroupPrimitive.Indicator asChild>
            <div className="flex items-center justify-center w-full h-full">
              <AnimatePresence>
                <motion.div
                  key="dot"
                  className="rounded-full bg-[hsl(var(--hu-primary))]"
                  style={{
                    width: dotSize,
                    height: dotSize,
                  }}
                  initial={{ scale: 0, opacity: 0 }}
                  animate={{ scale: 1, opacity: 1 }}
                  exit={{ scale: 0, opacity: 0 }}
                  transition={{
                    duration: 0.2,
                    ease: "easeInOut",
                  }}
                />
              </AnimatePresence>
            </div>
          </RadioGroupPrimitive.Indicator>
        </RadioGroupPrimitive.Item>

        {(label || description) && (
          <div className="grid gap-1.5 leading-none">
            {label && (
              <label
                htmlFor={itemId}
                className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
              >
                {label}
              </label>
            )}
            {description && (
              <p className="text-xs text-[hsl(var(--hu-muted-foreground))] peer-disabled:opacity-70">
                {description}
              </p>
            )}
          </div>
        )}
      </div>
    </div>
  );
});

RadioItem.displayName = "RadioItem";

export {
  RadioGroup,
  RadioItem,
  radioGroupVariants,
  radioVariants,
  type RadioGroupProps,
  type RadioItemProps,
};

Update your utils.ts file if you haven't already:

src/lib/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Coming soon...

Usage

import { RadioGroup, RadioItem } from "@/components/ui/Radio";
<RadioGroup defaultValue="option1">
  <RadioItem value="option1" label="Option 1" />
  <RadioItem value="option2" label="Option 2" />
  <RadioItem value="option3" label="Option 3" />
</RadioGroup>

Basic Radio Group

<RadioGroup defaultValue="option1">
  <RadioItem value="option1" label="Option 1" />
  <RadioItem value="option2" label="Option 2" />
  <RadioItem value="option3" label="Option 3" />
</RadioGroup>

With Description

Choose the option that best fits your needs

A relaxed and easy-going approach

Minimal space with essential features

Plenty of room with all amenities

<RadioGroup
  defaultValue="comfortable"
  label="Select your comfort level"
  description="Choose the option that best fits your needs"
>
  <RadioItem
    value="comfortable"
    label="Comfortable"
    description="A relaxed and easy-going approach"
  />
  <RadioItem
    value="compact"
    label="Compact"
    description="Minimal space with essential features"
  />
  <RadioItem
    value="spacious"
    label="Spacious"
    description="Plenty of room with all amenities"
  />
</RadioGroup>

Sizes

Small

Default

Large

<div className="space-y-6">
  <div>
    <h4 className="text-sm font-medium mb-3">Small</h4>
    <RadioGroup defaultValue="small1">
      <RadioItem value="small1" label="Small option 1" size="sm" />
      <RadioItem value="small2" label="Small option 2" size="sm" />
    </RadioGroup>
  </div>

  <div>
    <h4 className="text-sm font-medium mb-3">Default</h4>
    <RadioGroup defaultValue="default1">
      <RadioItem value="default1" label="Default option 1" />
      <RadioItem value="default2" label="Default option 2" />
    </RadioGroup>
  </div>

  <div>
    <h4 className="text-sm font-medium mb-3">Large</h4>
    <RadioGroup defaultValue="large1">
      <RadioItem value="large1" label="Large option 1" size="lg" />
      <RadioItem value="large2" label="Large option 2" size="lg" />
    </RadioGroup>
  </div>
</div>

Horizontal Layout

<RadioGroup
  defaultValue="yes"
  orientation="horizontal"
  label="Do you agree?"
>
  <RadioItem value="yes" label="Yes" />
  <RadioItem value="no" label="No" />
  <RadioItem value="maybe" label="Maybe" />
</RadioGroup>

Disabled State

<RadioGroup defaultValue="option1" disabled>
  <RadioItem value="option1" label="Disabled option 1" />
  <RadioItem value="option2" label="Disabled option 2" />
  <RadioItem value="option3" label="Disabled option 3" />
</RadioGroup>

With Error

Basic features

Advanced features

All features

Please select a plan to continue

<RadioGroup label="Select a plan" error="Please select a plan to continue">
  <RadioItem value="free" label="Free Plan" description="Basic features" />
  <RadioItem value="pro" label="Pro Plan" description="Advanced features" />
  <RadioItem
    value="enterprise"
    label="Enterprise Plan"
    description="All features"
  />
</RadioGroup>

Controlled

Currently selected: option2

<div className="space-y-4">
  <RadioGroup
    value={value}
    onValueChange={setValue}
    label="Controlled Radio Group"
    description={`Currently selected: ${value}`}
  >
    <RadioItem value="option1" label="Option 1" />
    <RadioItem value="option2" label="Option 2" />
    <RadioItem value="option3" label="Option 3" />
  </RadioGroup>

  <div className="flex gap-2">
    <button
      onClick={() => setValue("option1")}
      className="px-3 py-1 text-xs bg-gray-100 rounded"
    >
      Select Option 1
    </button>
    <button
      onClick={() => setValue("option2")}
      className="px-3 py-1 text-xs bg-gray-100 rounded"
    >
      Select Option 2
    </button>
    <button
      onClick={() => setValue("option3")}
      className="px-3 py-1 text-xs bg-gray-100 rounded"
    >
      Select Option 3
    </button>
  </div>
</div>
); ```

</Tab>
</Tabs>

### Payment Method Example

<Tabs items={["Preview", "Code"]}>
<Tab value="Preview">
<PreviewContainer>
  <PaymentMethodRadio />
</PreviewContainer>
</Tab>
<Tab value="Code">
```tsx
<RadioGroup
  defaultValue="card"
  label="Payment Method"
  description="Select your preferred payment method"
>
  <RadioItem
    value="card"
    label="Credit Card"
    description="Pay with Visa, Mastercard, or American Express"
  />
  <RadioItem
    value="paypal"
    label="PayPal"
    description="Pay with your PayPal account"
  />
  <RadioItem
    value="bank"
    label="Bank Transfer"
    description="Direct transfer from your bank account"
  />
  <RadioItem
    value="crypto"
    label="Cryptocurrency"
    description="Pay with Bitcoin, Ethereum, or other crypto"
  />
</RadioGroup>

API Reference

RadioGroup

PropTypeDefault
disabled?
boolean
-
onValueChange?
(value: string) => void
-
value?
string
-
defaultValue?
string
-
error?
string
-
description?
string
-
label?
string
-
orientation?
"vertical" | "horizontal"
"vertical"

RadioItem

PropTypeDefault
disabled?
boolean
-
description?
string
-
label?
string
-
size?
"sm" | "default" | "lg"
"default"
value
string
-
Edit on GitHub

Last updated on