Theme Toggler Button

A button that cycles the theme, its icon turning into the next one.

"use client"

import { type VariantProps } from "class-variance-authority"
import {
  ThemeTogglerButton,
  ThemeTogglerButtonIcon,
  type Resolved,
  type ThemeSelection,
  themeTogglerButtonVariants,
} from "@/components/animate-ui/buttons/theme-toggler"
import {
  ComputerIcon,
  Moon02Icon,
  Sun03Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useTheme } from "next-themes"

export default function ThemeTogglerButtonDemo({
  variant = "default",
  size = "default",
  system = false,
}: VariantProps<typeof themeTogglerButtonVariants> & {
  system?: boolean | string
}) {
  const { theme, resolvedTheme, setTheme } = useTheme()
  const withSystem = system === true || system === "true"

  return (
    <ThemeTogglerButton
      theme={(theme ?? "system") as ThemeSelection}
      resolvedTheme={(resolvedTheme ?? "light") as Resolved}
      setTheme={setTheme}
      modes={withSystem ? ["light", "dark", "system"] : ["light", "dark"]}
      aria-label="Toggle theme"
      variant={variant}
      size={size}
    >
      <ThemeTogglerButtonIcon
        light={<HugeiconsIcon icon={Sun03Icon} strokeWidth={2} />}
        dark={<HugeiconsIcon icon={Moon02Icon} strokeWidth={2} />}
        system={<HugeiconsIcon icon={ComputerIcon} strokeWidth={2} />}
      />
    </ThemeTogglerButton>
  )
}

Installation

npx shadcn@latest add @animate-ui/buttons-theme-toggler
pnpm dlx shadcn@latest add @animate-ui/buttons-theme-toggler
yarn dlx shadcn@latest add @animate-ui/buttons-theme-toggler
bun x shadcn@latest add @animate-ui/buttons-theme-toggler

Usage

Bring your own theme state. With next-themes:

import { useTheme } from 'next-themes';
import {
  ThemeTogglerButton,
  ThemeTogglerButtonIcon,
  type Resolved,
  type ThemeSelection,
} from '@/components/animate-ui/buttons/theme-toggler';

export function ThemeButton() {
  const { theme, resolvedTheme, setTheme } = useTheme();

  return (
    <ThemeTogglerButton
      theme={theme as ThemeSelection}
      resolvedTheme={resolvedTheme as Resolved}
      setTheme={setTheme}
      modes={['light', 'dark']}
      aria-label="Toggle theme"
    >
      <ThemeTogglerButtonIcon light={<SunIcon />} dark={<MoonIcon />} />
    </ThemeTogglerButton>
  );
}

Each click applies the next theme of modes at once, through setTheme. For a wipe across the page, use Theme Toggler. When modes leave out system, the button shows the theme actually displayed, and a system theme counts as the one it resolves to. Read the state in your own parts with useThemeTogglerButton().

Animation

  • The icon swaps as the theme changes: the old one turns 90° on as it shrinks to half its size into a light blur (blur(2px)) and fades out, while the new one turns in from 90° back and grows out of it, so both keep turning the same way. Over 0.3s with an ease-out (cubic-bezier(0.23, 1, 0.32, 1)), twice the Copy Button's swap, so the turn reads. Icons only swap once the button is used: on mount, and when the theme becomes known after hydrating, the icon shows at rest.
  • Pressing scales the button to tapScale (0.97), on Motion's spring for scale (stiffness: 550, damping: 30). It doesn't scale on hover, unless you set hoverScale.
  • <AnimateUIProvider tempo> paces the swap and the press spring.
  • Under reduced motion, the icons crossfade through their blur without turning or scaling, and the button doesn't scale.

API Reference

ThemeTogglerButton

PropTypeDefault
variant?
'default' | 'accent' | 'outline' | 'ghost' | null'default'
size?
'default' | 'sm' | 'lg' | null'default'
theme
'light' | 'dark' | 'system'-
resolvedTheme
'light' | 'dark'-
setTheme
(theme: 'light' | 'dark' | 'system') => void-
modes?
('light' | 'dark' | 'system')[]['light', 'dark', 'system']
hoverScale?
number1
tapScale?
number0.97
transition?
Transition{ type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 }
asChild?
booleanfalse
...props?
HTMLMotionProps<'button'>-

ThemeTogglerButtonIcon

PropTypeDefault
light
React.ReactNode-
dark
React.ReactNode-
system?
React.ReactNode-
rotate?
number90
transition?
Transition{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }
...props?
React.ComponentProps<'span'>-

Data attributes

PropTypeDefault
data-theme?
'light' | 'dark' | 'system'-

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

Last updated: 10/18/2026

On this page