Theme Toggler

A theme switch that wipes the new theme across the page with the View Transitions API.

"use client"

import {
  ThemeToggler,
  type Direction,
  type Resolved,
  type ThemeSelection,
} from "@animate-ui/react/primitives/effects/theme-toggler"
import {
  ComputerIcon,
  Moon02Icon,
  Sun03Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useTheme } from "next-themes"

import { Button } from "@/components/ui/button"

const NEXT: Record<ThemeSelection, ThemeSelection> = {
  light: "system",
  system: "dark",
  dark: "light",
}

const ICONS = { light: Sun03Icon, dark: Moon02Icon, system: ComputerIcon }

export default function ThemeTogglerDemo({
  direction = "ltr",
}: {
  direction?: Direction
}) {
  const { theme, resolvedTheme, setTheme } = useTheme()

  return (
    <ThemeToggler
      theme={(theme ?? "system") as ThemeSelection}
      resolvedTheme={(resolvedTheme ?? "light") as Resolved}
      setTheme={setTheme}
      direction={direction}
    >
      {({ effective, toggleTheme }) => (
        <Button
          variant="outline"
          size="icon-lg"
          aria-label={`Theme: ${effective}. Switch to ${NEXT[effective]}`}
          onClick={() => toggleTheme(NEXT[effective])}
        >
          <HugeiconsIcon icon={ICONS[effective]} strokeWidth={2} />
        </Button>
      )}
    </ThemeToggler>
  )
}

Installation

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

Usage

Bring your own theme state: ThemeToggler takes the theme and its setter as props, so it works with any theme library. With next-themes:

import { useTheme } from 'next-themes';
import {
  ThemeToggler,
  type Resolved,
  type ThemeSelection,
} from '@animate-ui/react/primitives/effects/theme-toggler';

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

  return (
    <ThemeToggler
      theme={theme as ThemeSelection}
      resolvedTheme={resolvedTheme as Resolved}
      setTheme={setTheme}
    >
      {({ effective, toggleTheme }) => (
        <button
          onClick={() => toggleTheme(effective === 'dark' ? 'light' : 'dark')}
        >
          {effective === 'dark' ? 'Light mode' : 'Dark mode'}
        </button>
      )}
    </ThemeToggler>
  );
}

During the wipe, the dark class is toggled on <html>: this matches class-based dark mode (Tailwind's default, next-themes with attribute="class").

Animation

  • The new theme is revealed with a clip-path wipe over 700ms with an ease-in-out, in direction (left to right by default). The old and new pages are snapshots, so nothing re-renders mid-wipe.
  • setTheme is called once the wipe finishes; onImmediateChange fires right away.
  • Switching to system when it resolves to the current theme changes nothing on screen, so there is no wipe.
  • Browsers without the View Transitions API swap the theme at once.
  • <AnimateUIProvider tempo> paces the wipe.
  • Under reduced motion, the theme swaps at once, without the wipe.
  • On the server and while hydrating, the render function receives effective: 'system' and resolved: 'light', since the theme is only known on the client.

API Reference

ThemeToggler

PropTypeDefault
theme
'light' | 'dark' | 'system'-
resolvedTheme
'light' | 'dark'-
setTheme
(theme: 'light' | 'dark' | 'system') => void-
direction?
'btt' | 'ttb' | 'ltr' | 'rtl''ltr'
onImmediateChange?
(theme: 'light' | 'dark' | 'system') => void-
children?
React.ReactNode | ((state: { resolved: 'light' | 'dark'; effective: 'light' | 'dark' | 'system'; toggleTheme: (theme: 'light' | 'dark' | 'system') => void }) => React.ReactNode)-

Credits

  • Inspired by Magic UI's animated theme toggler.

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

Last updated: 10/18/2026

On this page