Toggle Switch

Free

An accessible on/off switch built on a native button.

npx shadcn@latest add @ui-shrushank/toggle-switch

The install writes a single file to components/ui/ToggleSwitch.tsx, which exports ToggleSwitch.

tsx
import { ToggleSwitch } from "@/components/ui/ToggleSwitch";
tsx
<ToggleSwitch label="Label" />

Needs framer-motion installed. The shadcn CLI adds it for you.

The on/off control people most often fake with a styled checkbox or a plain div.

Built on a native <button> with role="switch" and aria-checked; the knob slides, the track colour changes.

Every line below was read out of the file you are about to copy, so you can check each one against the source at the bottom of this page.

  • Uses the native role="switch" semantics rather than a styled div, so assistive technology announces what the control is and how it behaves.
  • Carries aria-checked, so its state is exposed and not left to the visual treatment alone.
  • Has an explicit prefers-reduced-motion branch: the animation is dropped for an instant state change, and the component stays fully usable without it.
  • Draws a focus-visible ring that is separate from its hover treatment, so keyboard users get an affordance mouse users do not take away.
  • Pairs every input with a real <label>, so the field keeps its name after the placeholder disappears.

role="switch" tells a screen reader "this takes effect immediately" (vs. a checkbox that's submitted later). The semantic difference matters for the user's mental model.

1 required, 0 optional.

labelrequired
string

One file, no runtime package. Copy it, or install it with the command at the top of this page.

ToggleSwitch.tsx
"use client";import { useState } from "react";import { motion, useReducedMotion } from "framer-motion";export function ToggleSwitch({ label }: { label: string }) {  const [checked, setChecked] = useState(false);  const shouldReduceMotion = useReducedMotion();  return (    <label className="flex items-center gap-3 cursor-pointer select-none">      <motion.button        role="switch"        aria-checked={checked}        onClick={() => setChecked((v) => !v)}        whileTap={{ scale: 0.93 }}        className={`relative h-6 w-11 shrink-0 rounded-full transition-colors duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 ${          checked ? "bg-emerald-600" : "bg-stone-300 dark:bg-stone-600"        }`}      >        <motion.span          animate={{ x: checked ? 22 : 2 }}          transition={            shouldReduceMotion              ? { duration: 0 }              : { type: "spring", stiffness: 700, damping: 32 }          }          className="absolute top-0.5 block h-5 w-5 rounded-full bg-white shadow-md"        />      </motion.button>      <span className="text-sm text-stone-700 dark:text-stone-200">{label}</span>    </label>  );}