Switch

Toggle switch built on Base UI. Supports controlled and uncontrolled modes with optional label.

API Reference

PropTypeDefault
checkedboolean
defaultCheckedbooleanfalse
onCheckedChange(checked: boolean) => void
disabledbooleanfalse
labelstring
namestring
aria-labelstring"Toggle"

Label

<Switch label="Dark mode" defaultChecked />
<Switch label="Notifications" />

Disabled

<Switch disabled defaultChecked />
<Switch disabled />
<Switch label="With label" disabled />

Usage Notes

  • This is a pre-styled wrapper around Base UI Switch. Refer to their docs for the full API.
  • When label is provided, the switch is wrapped in a <label> element automatically.

Basic Usage

import { Switch } from "@/ui";

<Switch label="Enable notifications" />

Source

src/ui/Switch.tsx
"use client";

import { Switch as BaseSwitch } from "@base-ui/react/switch";
import { cn } from "@/utils";

interface SwitchProps {
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
  disabled?: boolean;
  label?: string;
  name?: string;
  className?: string;
  "aria-label"?: string;
}

export function Switch({
  checked,
  defaultChecked,
  onCheckedChange,
  disabled,
  label,
  name,
  className,
  "aria-label": ariaLabel,
}: SwitchProps) {
  const track = (
    <BaseSwitch.Root
      checked={checked}
      defaultChecked={defaultChecked}
      onCheckedChange={onCheckedChange}
      disabled={disabled}
      name={name}
      aria-label={label ? undefined : (ariaLabel ?? "Toggle")}
      className={cn(
        "relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full bg-root-200 transition-colors",
        "data-checked:bg-primary",
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
        "disabled:cursor-not-allowed disabled:opacity-50",
        !label && className,
      )}
    >
      <BaseSwitch.Thumb className="size-5 translate-x-0.5 rounded-full bg-root shadow-sm transition-transform data-checked:translate-x-5.5" />
    </BaseSwitch.Root>
  );

  if (!label) return track;

  return (
    <label
      className={cn(
        "inline-flex items-center gap-2",
        disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
        className,
      )}
    >
      {track}
      <span className="text-sm text-root-contrast">{label}</span>
    </label>
  );
}