Switch

Toggle switch, standalone or as a labelled row.

import { Switch, SwitchRow }from "@/components/ui"

Standalone & row

Full source

The complete component, read from src/components/ui/switch.tsx.

switch.tsx
"use client";

import { cn } from "@/lib/utils";

export interface SwitchProps {
  checked: boolean;
  onChange: (checked: boolean) => void;

  size?: "sm" | "md";
  disabled?: boolean;

  label?: string;
}

export function Switch({ checked, onChange, size = "md", disabled, label }: SwitchProps) {
  const trackW = size === "sm" ? 28 : 36;
  const trackH = size === "sm" ? 16 : 20;
  const thumbPx = size === "sm" ? 12 : 16;
  const onX = trackW - thumbPx - 2;

  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      aria-label={label}
      disabled={disabled}
      onClick={() => !disabled && onChange(!checked)}
      className={cn(
        "relative inline-flex shrink-0 items-center rounded-full transition-colors duration-150",
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1",
        checked ? "bg-accent" : "bg-border",
        disabled && "cursor-not-allowed opacity-50"
      )}
      style={{ width: trackW, height: trackH }}
    >
      <span
        className="inline-block rounded-full bg-surface shadow transition-transform duration-150"
        style={{
          width: thumbPx,
          height: thumbPx,
          transform: `translateX(${checked ? onX : 2}px)`,
        }}
      />
    </button>
  );
}

export interface SwitchRowProps {
  label: React.ReactNode;
  hint?: string;
  children: React.ReactNode;

  labelRight?: boolean;
}

export function SwitchRow({ label, hint, children, labelRight }: SwitchRowProps) {
  return (
    <label
      className={cn(
        "flex cursor-pointer items-center gap-3",
        labelRight ? "flex-row" : "flex-row-reverse justify-end"
      )}
    >
      {children}
      <span className="min-w-0">
        <span className="block text-sm font-medium text-text">{label}</span>
        {hint && <span className="block text-xs text-text-tertiary">{hint}</span>}
      </span>
    </label>
  );
}