Skip to content
    Style: Wireframe

    Components · Forms

    Radio group

    Native radios in a fieldset, with hints: one Tab stop, arrow keys choose. No JavaScript.

    1.4% of the 2,073 sites in The UI Index have a radio (28 sites).

    Deploy

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/radio-group.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/radio-group.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    radio-group.tsx
    import type { ChangeEvent } from 'react';
    import { cn } from '@/lib/utils';
    import { radioGroup, type RadioOption } from './radio-group.variants';
    
    export type { RadioOption };
    
    export interface RadioGroupProps {
      /** The group's name (the form field) and, with the option value, the radios' ids. */
      name: string;
      legend: string;
      options: RadioOption[];
      /** Controlled value (with onChange); use defaultValue for an uncontrolled group. */
      value?: string;
      defaultValue?: string;
      onChange?: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
      /** Prefix for element ids; defaults to the name. */
      id?: string;
      className?: string;
    }
    
    /** Native radios in a fieldset: one Tab stop, arrow keys move and select. */
    export function RadioGroup({ name, legend, options, value, defaultValue, onChange, id = name, className }: RadioGroupProps) {
      return (
        <fieldset className={cn(radioGroup.root, className)}>
          <legend className={radioGroup.legend}>{legend}</legend>
          {options.map((o) => (
            <div key={o.value} className={radioGroup.option}>
              <input
                type="radio"
                id={`${id}-${o.value}`}
                name={name}
                value={o.value}
                {...(value !== undefined ? { checked: o.value === value, onChange: (e: ChangeEvent<HTMLInputElement>) => onChange?.(o.value, e) } : { defaultChecked: o.value === defaultValue, onChange: onChange && ((e: ChangeEvent<HTMLInputElement>) => onChange(o.value, e)) })}
                disabled={o.disabled}
                aria-describedby={o.hint ? `${id}-${o.value}-hint` : undefined}
                className={radioGroup.radio}
              />
              <label htmlFor={`${id}-${o.value}`} className={radioGroup.label}>
                {o.label}
                {o.hint && <span id={`${id}-${o.value}-hint`} className={radioGroup.hint}>{o.hint}</span>}
              </label>
            </div>
          ))}
        </fieldset>
      );
    }
    radio-group.variants.ts
    /** Native radio buttons in a fieldset: arrow keys move and select within the group, one Tab stop, no JavaScript. */
    export const radioGroup = {
      root: 'm-0 grid min-w-0 gap-3 border-0 p-0',
      legend: 'mb-1 p-0 text-sm font-medium',
      option: 'flex items-start gap-3 text-sm',
      radio:
        "peer mt-0.5 grid size-4 shrink-0 cursor-pointer appearance-none place-content-center rounded-full border border-input bg-background transition-[border-color,transform] before:size-2 before:scale-0 before:rounded-full before:bg-primary before:transition-transform before:content-[''] enabled:not-checked:hover:border-foreground enabled:not-checked:hover:bg-accent enabled:active:scale-90 enabled:not-checked:active:bg-pressed checked:enabled:hover:before:scale-[0.8] checked:enabled:active:border-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:active:before:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:active:before:scale-100 checked:border-primary checked:before:scale-100 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
      label: 'cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
      hint: 'block text-muted-foreground',
    };
    
    export type RadioOption = { value: string; label: string; hint?: string; disabled?: boolean };