GitHub

Radio Group

Mutually exclusive options with clear active state and descriptive labels.

01 Live Demo

02 Code Snippet

src/ui/RadioGroup.tsx
import { RadioGroup } from "@/ui/RadioGroup";

<RadioGroup.Root value={plan} onValueChange={setPlan}>
  <RadioGroup.Item value="starter" label="Starter" description="Best for side projects" />
  <RadioGroup.Item value="pro" label="Pro" description="For production apps" />
  <RadioGroup.Item value="enterprise" label="Enterprise" description="Advanced controls" />
</RadioGroup.Root>

03 Copy-Paste (Single File)

RadioGroup.tsx
import { createContext, useContext, useState, type ReactNode } from "react";

type Ctx = { value: string; setValue: (value: string) => void };
const Ctx = createContext<Ctx | null>(null);

function useRadio() {
  const context = useContext(Ctx);
  if (!context) throw new Error("Use inside RadioGroup.Root");
  return context;
}

function RadioGroupRoot({ value, defaultValue = "", onValueChange, children }: { value?: string; defaultValue?: string; onValueChange?: (value: string) => void; children: ReactNode }) {
  const [internal, setInternal] = useState(defaultValue);
  const active = value ?? internal;
  const setValue = (next: string) => {
    if (value === undefined) setInternal(next);
    onValueChange?.(next);
  };
  return <Ctx.Provider value={{ value: active, setValue }}><div className="space-y-2">{children}</div></Ctx.Provider>;
}

function RadioGroupItem({ value, label, description }: { value: string; label: string; description?: string }) {
  const radio = useRadio();
  const checked = radio.value === value;
  return (
    <button type="button" role="radio" aria-checked={checked} onClick={() => radio.setValue(value)} className={"flex w-full items-start gap-3 rounded-lg border p-3 text-left " + (checked ? "border-primary bg-primary/5" : "border-slate-200")}>
      <span className={"mt-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full border " + (checked ? "border-primary" : "border-slate-400")}>{checked && <span className="h-2 w-2 rounded-full bg-primary" />}</span>
      <span>
        <span className="block text-sm font-medium text-slate-900 dark:text-white">{label}</span>
        {description && <span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">{description}</span>}
      </span>
    </button>
  );
}

type RadioGroupCompound = typeof RadioGroupRoot & { Root: typeof RadioGroupRoot; Item: typeof RadioGroupItem };
export const RadioGroup = Object.assign(RadioGroupRoot, { Root: RadioGroupRoot, Item: RadioGroupItem }) as RadioGroupCompound;
react-principles