RadioGroup

A set of checkable radio buttons where no more than one can be checked.

Demo

Dependencies

lucide-reactclsxtailwind-merge

Props

This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().

Source

"use client";

import { createContext, useContext, useCallback } from "react";
import { cn } from "@/lib/utils";
import { Circle } from "lucide-react";

interface RadioContextValue {
  value: string;
  onValueChange: (value: string) => void;
}

const RadioContext = createContext<RadioContextValue | null>(null);

export interface RadioGroupProps {
  value?: string;
  defaultValue?: string;
  onValueChange?: (value: string) => void;
  children?: React.ReactNode;
  className?: string;
}

export function RadioGroup({
  value: controlledValue,
  defaultValue,
  onValueChange,
  children,
  className,
}: RadioGroupProps) {
  const value = controlledValue ?? defaultValue ?? "";
  return (
    <RadioContext.Provider
      value={{ value, onValueChange: onValueChange ?? (() => {}) }}
    >
      <div role="radiogroup" className={cn("space-y-2", className)}>
        {children}
      </div>
    </RadioContext.Provider>
  );
}

export function RadioGroupItem({
  value,
  children,
  className,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string }) {
  const ctx = useContext(RadioContext);
  const checked = ctx?.value === value;

  const handleClick = useCallback(() => {
    ctx?.onValueChange(value);
  }, [ctx, value]);

  return (
    <label
      className={cn(
        "flex cursor-pointer items-center gap-2 text-sm text-gray-700",
        className,
      )}
    >
      <button
        role="radio"
        aria-checked={checked}
        onClick={handleClick}
        className={cn(
          "flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-gray-300 bg-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
          checked && "border-gray-900",
        )}
        {...props}
      >
        {checked && (
          <Circle className="h-2.5 w-2.5 fill-gray-900 text-gray-900" />
        )}
      </button>
      {children}
    </label>
  );
}

CLI

npx genui add radio-group