Actions

Toggle

UI button for boolean states, with no form behind it

Preview

size
label
disabled

Code

import css from "./Toggle.module.css";
import { useState } from "react";

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

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

interface ToggleProps {
  checked: boolean;
  size: Size | ResponsiveSize;
  label?: string;
  "aria-label"?: string;
  disabled?: boolean;
  onChange: (checked: boolean) => 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 Toggle({
  checked,
  size,
  label,
  "aria-label": ariaLabel,
  disabled,
  onChange,
}: ToggleProps) {
  const [interacted, setInteracted] = useState(false);

  const toggle = () => {
    if (disabled) return;
    setInteracted(true);
    onChange(!checked);
  };

  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      aria-label={label ? undefined : ariaLabel}
      disabled={disabled}
      className={`${css.toggle} ${getSizeClasses(size)} ${checked ? css.on : ""} ${disabled ? css.disabled : ""}`}
      onClick={toggle}
    >
      <span className={css.toggle_track}>
        <span
          className={`${css.track_dot} ${interacted ? css.animated : ""}`}
        ></span>
      </span>
      {label && <span className={css.toggle_label}>{label}</span>}
    </button>
  );
}

Props

PropTypeRequiredDescription
size"xs" | "sm" | "md" | "lg" | ResponsiveSizerequiredToggle size (track, dot and label). Fixed or responsive.
labelstring—Text next to the toggle. Without a label, only the track remains.
aria-labelstring—Name of the control for screen readers. Required when there is no label; ignored when there is one.
disabledboolean—Disables the toggle (not interactive, reduced opacity).
checkedbooleanrequiredOn state (controlled).
onChange(checked: boolean) => voidrequiredCallback with the new state on every change.