Toggle

A two-state button that can be either on or off.

"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { Toggle as TogglePrimitive } from "@animate-ui/react/primitives/radix-ui"
import { BookmarkIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { motion, useReducedMotion } from "motion/react"

const toggleVariants = cva(
  "hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle inline-flex items-center justify-center whitespace-nowrap outline-none hover:bg-muted focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        outline: "border-input hover:bg-muted border bg-transparent",
      },
      size: {
        default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
        lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Toggle({
  className,
  variant = "default",
  size = "default",
  ...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
  VariantProps<typeof toggleVariants>) {
  return (
    <TogglePrimitive.Root
      data-slot="toggle"
      className={cn(toggleVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Toggle, toggleVariants }

export default function ToggleDemo({
  size = "sm",
}: {
  size?: "default" | "sm" | "lg"
}) {
  const [pressed, setPressed] = React.useState(false)
  const reduce = useReducedMotion()

  return (
    <Toggle
      aria-label="Toggle bookmark"
      size={size}
      variant="outline"
      pressed={pressed}
      onPressedChange={setPressed}
    >
      <motion.span
        className="inline-flex"
        initial={false}
        animate={{
          scale: reduce ? 1 : pressed ? [1, 1.2, 1] : [1, 0.9, 1],
        }}
        transition={{
          duration: 0.35,
          times: [0, 0.4, 1],
          ease: ["easeOut", [0.34, 1.56, 0.64, 1]],
        }}
      >
        <HugeiconsIcon
          icon={BookmarkIcon}
          strokeWidth={2}
          className="fill-transparent transition-[fill] duration-200 ease-out group-data-[state=on]/toggle:fill-foreground"
        />
      </motion.span>
      Bookmark
    </Toggle>
  )
}

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 toggle
pnpm dlx shadcn@latest add toggle
yarn dlx shadcn@latest add toggle
bun x shadcn@latest add toggle

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 toggle
pnpm dlx @animate-ui/cli@latest animate toggle
yarn dlx @animate-ui/cli@latest animate toggle
bun x @animate-ui/cli@latest animate toggle

Usage

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

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

export function BoldToggle() {
  return (
    <Toggle.Root aria-label="Toggle bold">
      <BoldIcon />
    </Toggle.Root>
  );
}

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

Animation

The motion of Animate UI v1's toggle:

  • Toggle scales to 0.97 while pressed, on Motion's default spring for scale (stiffness 550, damping 30). No hover scale.
  • The press scale runs on the compositor (a transform), so shadcn/ui's transition-all doesn't tween its frames.
  • The on/off colours are the component's CSS: shadcn/ui colours the toggle itself (data-[state=on]:bg-muted) and fades it with transition-all. v1's styled toggle faded a separate highlight in and out instead (0.3s).
  • Toggling with Space changes the state at once.
  • Under reduced motion, there is no press scale.

API Reference

Toggle.Root takes exactly the props of Radix UI's Toggle, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 }

With asChild, it renders your element with Radix's behaviour, without the animation. AnimateUIProvider's tempo scales these transitions with everything else.

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

Last updated: 10/18/2026

On this page