Switch

A control that allows the user to toggle between checked and not checked.

"use client"

import * as React from "react"
import { cn } from "cn"
import { Switch as SwitchPrimitive } from "@animate-ui/react/primitives/radix-ui"
import { Label } from "@/components/ui/label"

function Switch({
  className,
  size = "default",
  ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
  size?: "sm" | "default"
}) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      data-size={size}
      className={cn(
        "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:border-transparent data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50",
        className
      )}
      {...props}
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0"
      />
    </SwitchPrimitive.Root>
  )
}

export { Switch }

export default function SwitchDemo() {
  return (
    <div className="flex items-center space-x-2">
      <Switch id="airplane-mode" />
      <Label htmlFor="airplane-mode">Airplane Mode</Label>
    </div>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add switch
pnpm dlx shadcn@latest add switch
yarn dlx shadcn@latest add switch
bun x shadcn@latest add switch

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate switch
pnpm dlx @animate-ui/cli@latest animate switch
yarn dlx @animate-ui/cli@latest animate switch
bun x @animate-ui/cli@latest animate switch

Usage

Import the primitive from Animate UI instead of Radix UI. Everything else stays the same.

import { Switch } from '@animate-ui/react/primitives/radix-ui';

export function AirplaneMode() {
  return (
    <label className="flex items-center gap-2">
      <Switch.Root>
        <Switch.Thumb />
      </Switch.Root>
      Airplane mode
    </label>
  );
}

Coming from the @radix-ui/react-switch package? Use the subpath: import * as SwitchPrimitive from '@animate-ui/react/primitives/radix-ui/switch'.

Animation

The motion of Animate UI v1's switch:

  • Thumb, pressed: stretches toward the middle of the track, its other edge in place. Its width grows by 3/16 (a 16px thumb becomes 19px, v1's pressedWidth) on a spring (stiffness 300, damping 25). It's the thumb's real width, so its round ends stay round.
  • Thumb, on a change: springs to its new side while its width springs back, on the same spring (v1's layout animation). The thumb's translate classes still decide where it rests (data-checked:translate-x-…): Animate UI puts it back where it was with a transform and springs it to its new place.
  • Root doesn't move. Its colours are the component's CSS, and v1's track changed colour at once: shadcn/ui's transition-all on the track would fade it, so the CLI removes it.
  • The press is v1's: the pointer, or Enter on the keyboard. Space toggles without the stretch, and the thumb glides the same way.
  • Toggling again mid-glide turns back from where the thumb is, without a jump.
  • Under reduced motion, the thumb jumps to its side, without the stretch.
  • shadcn/ui's transition-transform on the thumb would tween its translate; the CLI removes it too (Animate UI also skips it while measuring, so a leftover class doesn't break the glide).

API Reference

Every part takes exactly the props of Radix UI's Switch. Animate UI adds the animation props below. With asChild, a part renders your element with Radix's behaviour: an asChild Root has no press stretch, an asChild Thumb no animation.

Root

Radix UI props. The Root doesn't animate itself: it tells the Thumb when it's pressed.

Thumb

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 300, damping: 25 }

AnimateUIProvider's tempo scales the spring with everything else.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page