Forms

Radio

Option button for single-selection groups

Preview

size
labels
required
error
disabled

Code

import css from "./Radio.module.css";

type Size = "xs" | "sm" | "md" | "lg";

interface ResponsiveSize {
  base: Size;
  mobile?: Size;
  tablet?: Size;
  desktop?: Size;
  wide?: Size;
}

interface RadioProps {
  size: Size | ResponsiveSize;
  name: string;
  value: string;
  label?: string;
  "aria-label"?: string;
  checked: boolean;
  required?: boolean;
  error?: boolean;
  disabled?: boolean;
  onChange: (value: string) => void;
}

function getSizeClasses(size: Size | ResponsiveSize): string {
  if (typeof size === "string") {
    return css[size];
  }

  const classes = [css[`${size.base}_base`]];

  if (size.mobile) classes.push(css[`${size.mobile}_mobile`]);
  if (size.tablet) classes.push(css[`${size.tablet}_tablet`]);
  if (size.desktop) classes.push(css[`${size.desktop}_desktop`]);
  if (size.wide) classes.push(css[`${size.wide}_wide`]);

  return classes.join(" ");
}

export default function Radio({
  size,
  name,
  value,
  label,
  "aria-label": ariaLabel,
  checked,
  required,
  error,
  disabled,
  onChange,
}: RadioProps) {
  const handleChange = () => {
    onChange(value);
  };

  const Wrapper = label ? "label" : "span";

  return (
    <Wrapper
      className={`
        ${css.radio}
        ${getSizeClasses(size)}
        ${checked ? css.checked : ""}
        ${error ? css.error : ""}
        ${disabled ? css.disabled : ""}
      `}
    >
      <input
        type="radio"
        name={name}
        value={value}
        checked={checked}
        required={required}
        disabled={disabled}
        aria-label={label ? undefined : ariaLabel}
        aria-invalid={error}
        onChange={handleChange}
      />
      <span className={css.radio_circle}>
        <span className={css.circle_dot}></span>
      </span>
      {label && (
        <span className={css.radio_label}>
          {label}
          {required && <span aria-hidden="true">*</span>}
        </span>
      )}
    </Wrapper>
  );
}

Props

PropTypeRequiredDescription
size"xs" | "sm" | "md" | "lg" | ResponsiveSizerequiredControl size (circle, dot and label). Fixed or responsive.
namestringrequiredGroup name. Radios sharing a name form one group.
valuestringrequiredValue identifying this option within the group.
labelstring—Text next to the radio. Without a label, only the circle remains.
aria-labelstring—Name of the control for screen readers. Required when there is no label; ignored when there is one.
requiredboolean—Marks the group as required (adds * to the label and the native required).
errorboolean—Visual error state (red border, aria-invalid).
disabledboolean—Disables the option (not interactive, reduced opacity).
checkedbooleanrequiredWhether this is the selected option of the group (controlled).
onChange(value: string) => voidrequiredCallback with the value of the chosen option.