Theme Toggler Button

PreviousNext

Icon button that switches theme with a full-page reveal animation, powered by the View Transitions API.

Circle
Circle blur
Iris
Polygon
Slide
Fade
"use client";

import {
  ThemeTogglerButton,
  type ThemeTogglerButtonVariant,
} from "@/components/matos-ui/theme-toggler-button";

const variants: { variant: ThemeTogglerButtonVariant; label: string }[] = [
  { variant: "circle", label: "Circle" },
  { variant: "circle-blur", label: "Circle blur" },
  { variant: "iris", label: "Iris" },
  { variant: "polygon", label: "Polygon" },
  { variant: "slide", label: "Slide" },
  { variant: "fade", label: "Fade" },
];

export function ThemeTogglerButtonDemo() {
  return (
    <div className="flex w-full max-w-2xl flex-col gap-5">
      <div className="flex flex-wrap items-center gap-3">
        {variants.map(({ variant, label }) => (
          <div
            key={variant}
            className="flex flex-col items-center gap-2 rounded-lg border border-border/60 px-4 py-3"
          >
            <ThemeTogglerButton
              variant={variant}
              aria-label={`Toggle theme (${label})`}
            />
            <span className="text-muted-foreground text-xs">{label}</span>
          </div>
        ))}
      </div>

      <div className="flex flex-wrap items-center gap-3 border-border/60 border-t pt-4">
        <ThemeTogglerButton size="sm" variant="polygon" direction="ltr" />
        <ThemeTogglerButton size="md" variant="polygon" direction="rtl" />
        <ThemeTogglerButton size="lg" variant="slide" direction="ttb" />
        <ThemeTogglerButton
          size="icon"
          variant="circle-blur"
          direction="btt"
          modes={["light", "dark", "system"]}
        />
      </div>
    </div>
  );
}

When To Use

Use ThemeTogglerButton anywhere you need a light/dark (or light/dark/system) switch that feels like more than a plain icon swap. When the browser supports the View Transitions API, clicking the button plays a full-page reveal animation seeded at the button's own position; browsers without support (and users with reduced motion enabled) fall back to an instant theme change, so the component is safe to use everywhere.

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/theme-toggler-button.json

Usage

import { ThemeTogglerButton } from "@/components/matos-ui/theme-toggler-button"

<ThemeTogglerButton />

Requires a ThemeProvider from next-themes (with attribute="class") somewhere above it in the tree.

Animation Variants

variant controls the reveal animation played on the incoming theme:

  • circle — a circle expands outward from the button, revealing the new theme (default).
  • circle-blur — the same circle reveal, softened with a blur-to-sharp transition.
  • iris — a circle reveal with a spring overshoot, like a camera iris snapping into focus.
  • polygon — a diagonal wipe sweeps across the screen.
  • slide — a straight curtain wipe, axis-aligned to direction.
  • fade — a plain crossfade, also used automatically as the safe fallback.
<ThemeTogglerButton variant="iris" />
<ThemeTogglerButton variant="polygon" direction="rtl" />
<ThemeTogglerButton variant="slide" direction="ttb" />

direction ("ltr" | "rtl" | "ttb" | "btt") sets the sweep axis for polygon and slide, and the rotation direction of the icon swap. It has no visible effect on circle, circle-blur, iris, or fade, which always originate from the button itself.

Three-Way Toggle

Pass modes to cycle through light, dark, and system instead of just light/dark. The icon switches to a monitor glyph while system is active.

<ThemeTogglerButton modes={["light", "dark", "system"]} />

Custom Icons

Override any of the icons per mode:

import { CloudMoon, CloudSun } from "lucide-react"

<ThemeTogglerButton
  icons={{ light: <CloudSun />, dark: <CloudMoon /> }}
/>

Reference

Props

PropTypeDefault
variant"circle" | "circle-blur" | "iris" | "polygon" | "slide" | "fade""circle"
size"sm" | "md" | "lg" | "icon""md"
direction"ltr" | "rtl" | "ttb" | "btt""ltr"
modes("light" | "dark" | "system")[]["light", "dark"]
durationnumber650
iconsPartial<Record<"light" | "dark" | "system", ReactNode>>-

For icon-only buttons the component already sets an aria-label describing the next theme; pass your own aria-label to override it.

Also exported: themeTogglerButtonVariants, ThemeTogglerButtonVariant, ThemeTogglerButtonDirection, ThemeTogglerButtonMode, and ThemeTogglerButtonProps.