Skip to Content
ComponentsRadio Group

Radio Group

Mutually exclusive option selector. Compound sub-components: RadioGroup + RadioGroup.Item. Uses React Context for shared selection state.

Design Language Comparison

Neobrutalism
Shadcn-inspired
Flowbite-inspired
Glassmorphism
Material Design 3
Neumorphism

Neobrutalism

Circular thick border, yellow fill, black center dot, bold label.

const NeobrutalismRadioGroup = ({ value, defaultValue, onChange, name = "nb-radio", disabled = false, children }) => {
  const [selectedValue, setSelectedValue] = useState(value ?? defaultValue ?? null);
  return (
    <RadioGroupContext.Provider value={{ selectedValue, setSelectedValue, name, disabled, onChange }}>
      <div role="radiogroup" className="flex flex-col gap-3 font-sans">{children}</div>
    </RadioGroupContext.Provider>
  );
};

NeobrutalismRadioGroup.Item = ({ value, label, disabled: itemDisabled = false }) => {
  const { selectedValue, setSelectedValue, name, disabled: groupDisabled, onChange } = useRadioGroup();
  const isChecked = selectedValue === value;
  const isDisabled = groupDisabled || itemDisabled;
  return (
    <label htmlFor={`${name}-${value}`} className={`inline-flex items-center gap-2.5 font-sans cursor-pointer select-none${isDisabled ? " cursor-not-allowed opacity-60" : ""}`}>
      <span className="relative flex-shrink-0 w-5 h-5">
        <input
          id={`${name}-${value}`}
          type="radio" name={name} value={value} checked={isChecked} disabled={isDisabled}
          onChange={() => { setSelectedValue(value); onChange?.(value); }}
          className="absolute opacity-0 w-0 h-0"
        />
        <span className={`block w-5 h-5 rounded-full border-[3px] border-black shadow-[3px_3px_0_#000] transition-colors ${isChecked ? "bg-yellow-400" : "bg-white"}`}>
          {isChecked && <span className="absolute inset-0 flex items-center justify-center"><span className="w-2 h-2 rounded-full bg-black" /></span>}
        </span>
      </span>
      <span className="text-[0.9375rem] font-extrabold text-black">{label}</span>
    </label>
  );
};

// Usage:
// <NeobrutalismRadioGroup name="fruit" defaultValue="apple" onChange={(v) => console.log(v)}>
//   <NeobrutalismRadioGroup.Item value="apple" label="Apple" />
//   <NeobrutalismRadioGroup.Item value="banana" label="Banana" />
//   <NeobrutalismRadioGroup.Item value="orange" label="Orange" />
// </NeobrutalismRadioGroup>

Shadcn-inspired

Thin zinc circle, dark filled dot, white center, muted label.

const ShadcnRadioGroup = ({ value, defaultValue, onChange, name = "shadcn-radio", disabled = false, children }) => {
  const [selectedValue, setSelectedValue] = useState(value ?? defaultValue ?? null);
  return (
    <RadioGroupContext.Provider value={{ selectedValue, setSelectedValue, name, disabled, onChange }}>
      <div role="radiogroup" className="flex flex-col gap-2.5 font-sans">{children}</div>
    </RadioGroupContext.Provider>
  );
};

ShadcnRadioGroup.Item = ({ value, label, disabled: itemDisabled = false }) => {
  const { selectedValue, setSelectedValue, name, disabled: groupDisabled, onChange } = useRadioGroup();
  const isChecked = selectedValue === value;
  const isDisabled = groupDisabled || itemDisabled;
  return (
    <label htmlFor={`${name}-${value}`} className={`inline-flex items-center gap-2 font-sans cursor-pointer select-none${isDisabled ? " cursor-not-allowed opacity-50" : ""}`}>
      <span className="relative flex-shrink-0 w-4 h-4">
        <input
          id={`${name}-${value}`}
          type="radio" name={name} value={value} checked={isChecked} disabled={isDisabled}
          onChange={() => { setSelectedValue(value); onChange?.(value); }}
          className="absolute opacity-0 w-0 h-0"
        />
        <span className={`block w-4 h-4 rounded-full border transition-colors duration-150 ${isChecked ? "bg-zinc-900 border-zinc-900" : "bg-white border-zinc-300"}`}>
          {isChecked && <span className="absolute inset-0 flex items-center justify-center"><span className="w-1.5 h-1.5 rounded-full bg-white" /></span>}
        </span>
      </span>
      <span className="text-sm font-medium text-zinc-900">{label}</span>
    </label>
  );
};

Flowbite-inspired

Blue filled dot (#1c64f2), white center, smooth transition.

const FlowbiteRadioGroup = ({ value, defaultValue, onChange, name = "fb-radio", disabled = false, children }) => {
  const [selectedValue, setSelectedValue] = useState(value ?? defaultValue ?? null);
  return (
    <RadioGroupContext.Provider value={{ selectedValue, setSelectedValue, name, disabled, onChange }}>
      <div role="radiogroup" className="flex flex-col gap-2.5 font-sans">{children}</div>
    </RadioGroupContext.Provider>
  );
};

FlowbiteRadioGroup.Item = ({ value, label, disabled: itemDisabled = false }) => {
  const { selectedValue, setSelectedValue, name, disabled: groupDisabled, onChange } = useRadioGroup();
  const isChecked = selectedValue === value;
  const isDisabled = groupDisabled || itemDisabled;
  return (
    <label htmlFor={`${name}-${value}`} className={`inline-flex items-center gap-2 font-sans cursor-pointer select-none${isDisabled ? " cursor-not-allowed opacity-65" : ""}`}>
      <span className="relative flex-shrink-0 w-4 h-4">
        <input
          id={`${name}-${value}`}
          type="radio" name={name} value={value} checked={isChecked} disabled={isDisabled}
          onChange={() => { setSelectedValue(value); onChange?.(value); }}
          className="absolute opacity-0 w-0 h-0"
        />
        <span className={`block w-4 h-4 rounded-full border transition-all duration-200 ${isChecked ? "bg-[#1c64f2] border-[#1c64f2]" : "bg-white border-gray-300"}`}>
          {isChecked && <span className="absolute inset-0 flex items-center justify-center"><span className="w-1.5 h-1.5 rounded-full bg-white" /></span>}
        </span>
      </span>
      <span className="text-sm font-medium text-gray-900">{label}</span>
    </label>
  );
};

Glassmorphism

Translucent frosted circle with white center dot on gradient background.

const GlassmorphismRadioGroup = ({ options = [], value, defaultValue, onChange, name = "glass-radio", disabled = false }) => {
  const [selected, setSelected] = useState(value ?? defaultValue ?? "");
  return (
    <fieldset className="flex flex-col gap-2 border-none p-0 m-0">
      {options.map((opt) => (
        <label key={opt.value} className={`inline-flex items-center gap-2.5 font-sans cursor-pointer${(disabled || opt.disabled) ? " cursor-not-allowed opacity-40" : ""}`}>
          <span className="relative flex-shrink-0 w-5 h-5">
            <input type="radio" name={name} value={opt.value} checked={selected === opt.value} disabled={disabled || opt.disabled}
              onChange={() => { setSelected(opt.value); onChange?.(opt.value); }}
              className="absolute opacity-0 w-0 h-0" />
            <span className={`block w-5 h-5 rounded-full border backdrop-blur-md transition-all duration-200 ${selected === opt.value ? "bg-white/40 border-white/60" : "bg-white/10 border-white/30"}`}>
              {selected === opt.value && <span className="absolute inset-0 flex items-center justify-center"><span className="w-2 h-2 rounded-full bg-white" /></span>}
            </span>
          </span>
          <span className="text-[0.9375rem] font-medium text-white">{opt.label}</span>
        </label>
      ))}
    </fieldset>
  );
};

Material Design 3

Purple bordered ring, filled dot on selection, smooth transition.

const Md3RadioGroup = ({ options = [], value, defaultValue, onChange, name = "md3-radio", disabled = false }) => {
  const [selected, setSelected] = useState(value ?? defaultValue ?? "");
  return (
    <fieldset className="flex flex-col gap-2 border-none p-0 m-0">
      {options.map((opt) => (
        <label key={opt.value} className={`inline-flex items-center gap-2.5 font-sans cursor-pointer${(disabled || opt.disabled) ? " cursor-not-allowed opacity-40" : ""}`}>
          <span className="relative flex-shrink-0 w-5 h-5">
            <input type="radio" name={name} value={opt.value} checked={selected === opt.value} disabled={disabled || opt.disabled}
              onChange={() => { setSelected(opt.value); onChange?.(opt.value); }}
              className="absolute opacity-0 w-0 h-0" />
            <span className={`block w-5 h-5 rounded-full border-2 transition-all duration-200 ${selected === opt.value ? "border-[#6750a4]" : "border-[#79747e]"}`}>
              {selected === opt.value && <span className="absolute inset-0 flex items-center justify-center"><span className="w-2.5 h-2.5 rounded-full bg-[#6750a4]" /></span>}
            </span>
          </span>
          <span className="text-[0.9375rem] font-medium text-[#1c1b1f]">{opt.label}</span>
        </label>
      ))}
    </fieldset>
  );
};

Neumorphism

Convex (raised) unselected, concave (inset) when selected, blue-gray dot.

const NmRadioGroup = ({ options = [], value, defaultValue, onChange, name = "nm-radio", disabled = false }) => {
  const [selected, setSelected] = useState(value ?? defaultValue ?? "");
  return (
    <fieldset className="flex flex-col gap-2 border-none p-0 m-0">
      {options.map((opt) => (
        <label key={opt.value} className={`inline-flex items-center gap-2.5 font-sans cursor-pointer${(disabled || opt.disabled) ? " cursor-not-allowed opacity-50" : ""}`}>
          <span className="relative flex-shrink-0 w-5 h-5">
            <input type="radio" name={name} value={opt.value} checked={selected === opt.value} disabled={disabled || opt.disabled}
              onChange={() => { setSelected(opt.value); onChange?.(opt.value); }}
              className="absolute opacity-0 w-0 h-0" />
            <span className={`block w-5 h-5 rounded-full bg-[#e0e5ec] transition-all duration-150 ${selected === opt.value ? "shadow-[inset_-3px_-3px_6px_#ffffff,_inset_3px_3px_6px_rgba(163,177,198,0.6)]" : "shadow-[-3px_-3px_6px_#ffffff,_3px_3px_6px_rgba(163,177,198,0.5)]"}`}>
              {selected === opt.value && <span className="absolute inset-0 flex items-center justify-center"><span className="w-2 h-2 rounded-full bg-[#6c7a9c]" /></span>}
            </span>
          </span>
          <span className="text-[0.9375rem] font-semibold text-[#6c7a9c]">{opt.label}</span>
        </label>
      ))}
    </fieldset>
  );
};
Last updated on

© 2026 UI Variants. Built with Nextra.