Studio KitInteractive Buttons and Controlsmotion/react

ToggleSwitch

Accessible on/off switch with a spring-driven thumb, three sizes, custom accent track color and both controlled and uncontrolled usage.

Preview

Customize

5 props

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add toggle-switch

Writes components/ui/toggle-switch.tsx and components/styles/toggle-switch.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { ToggleSwitch } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <ToggleSwitch />6  );7}

Props

PropTypeDefault
checked

Controlled on/off state.

booleanundefined
defaultChecked

Initial state when uncontrolled.

booleanfalse
onCheckedChange

Called with the next state when toggled.

(checked: boolean) => voidundefined
label

Visible label, also the accessible name.

string'Reduce motion'
size

Switch size.

'sm' | 'md' | 'lg''md'
accentColor

Track color when on.

string'#38bdf8'
disabled

Disables interaction.

booleanfalse
className

Extra class merged onto the root button.

string''

Accessibility

Renders a <button role="switch"> with aria-checked; the visible label is part of the button so it is the accessible name (falls back to aria-label "Toggle"). Space/Enter toggle natively, disabled uses the native attribute, and prefers-reduced-motion moves the thumb instantly.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/toggle-switch.tsx
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/toggle-switch.module.css";6 7export interface ToggleSwitchProps {8  /** Controlled on/off state */9  checked?: boolean;10  /** Initial state when uncontrolled */11  defaultChecked?: boolean;12  /** Called with the next state when toggled */13  onCheckedChange?: (checked: boolean) => void;14  /** Visible label (also the accessible name) */15  label?: string;16  /** Switch size */17  size?: "sm" | "md" | "lg";18  /** Track color when on */19  accentColor?: string;20  /** Disable interaction */21  disabled?: boolean;22  /** Optional className */23  className?: string;24}25 26const SIZES = {27  sm: { w: 36, h: 20 },28  md: { w: 48, h: 26 },29  lg: { w: 60, h: 32 },30} as const;31 32const PAD = 3;33 34export function ToggleSwitch({35  checked,36  defaultChecked = false,37  onCheckedChange,38  label = "Reduce motion",39  size = "md",40  accentColor = "#38bdf8",41  disabled = false,42  className = "",43}: ToggleSwitchProps) {44  const prefersReducedMotion = useReducedMotion();45  const [inner, setInner] = React.useState(defaultChecked);46  const isControlled = checked !== undefined;47  const on = isControlled ? checked : inner;48  const dims = SIZES[size] ?? SIZES.md;49  const thumb = dims.h - PAD * 2;50 51  const toggle = () => {52    if (disabled) return;53    const next = !on;54    if (!isControlled) setInner(next);55    onCheckedChange?.(next);56  };57 58  return (59    <button60      type="button"61      role="switch"62      aria-checked={on}63      aria-label={label ? undefined : "Toggle"}64      disabled={disabled}65      onClick={toggle}66      data-state={on ? "on" : "off"}67      style={68        {69          "--mf-ts-accent": accentColor,70          "--mf-ts-w": `${dims.w}px`,71          "--mf-ts-h": `${dims.h}px`,72          "--mf-ts-thumb": `${thumb}px`,73          "--mf-ts-pad": `${PAD}px`,74        } as React.CSSProperties75      }76      className={`${styles.root} ${styles[size] ?? ""} ${className}`.trim()}77    >78      <span className={styles.track} aria-hidden="true">79        <motion.span80          className={styles.thumb}81          initial={false}82          animate={{ x: on ? dims.w - dims.h : 0, scale: 1 }}83          whileTap={prefersReducedMotion || disabled ? undefined : { scale: 0.9 }}84          transition={prefersReducedMotion ? { duration: 0 } : { type: "spring", stiffness: 700, damping: 32 }}85        />86      </span>87      {label ? <span className={styles.label}>{label}</span> : null}88    </button>89  );90}