Inputs & Forms

Switch

A Radix Switch whose thumb slides with a spring-approximation ease and stretches subtly toward the travel direction while pressed; color changes instantly.

Install

npx shadcn@latest add @paragon/switch

switch.tsx

"use client";

import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";

export interface SwitchProps
  extends React.ComponentProps<typeof SwitchPrimitive.Root> {
  /** Disables the pressed-thumb stretch where the motion would distract. */
  static?: boolean;
}

/**
 * Radix Switch. The thumb slides with a zero-bounce spring approximation and
 * stretches ~2px toward the travel direction while pressed (via scaleX with a
 * state-dependent transform-origin, so it stays compositor-only). Track color
 * changes instantly — color is state, not motion.
 */
export function Switch({
  className,
  static: isStatic = false,
  ...props
}: SwitchProps) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      className={cn(
        "group inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-input",
        "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
        "data-[state=checked]:bg-primary",
        "disabled:cursor-not-allowed disabled:opacity-50",
        // Extend the hit area to 40px tall.
        "relative after:absolute after:top-1/2 after:left-1/2 after:h-10 after:w-full after:min-w-10 after:-translate-x-1/2 after:-translate-y-1/2",
        className,
      )}
      {...props}
    >
      <SwitchPrimitive.Thumb
        className={cn(
          "pointer-events-none block size-4 translate-x-0.5 rounded-full bg-background shadow-sm",
          "transition-[translate,scale] duration-200 [transition-timing-function:var(--ease-spring)] motion-reduce:transition-none",
          "data-[state=checked]:translate-x-[18px]",
          // Stretch anchors on the trailing edge so growth points toward travel.
          "origin-left data-[state=checked]:origin-right",
          "dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground",
          !isStatic && "group-active:scale-x-[1.125]",
        )}
      />
    </SwitchPrimitive.Root>
  );
}