Icon button that switches theme with a full-page reveal animation, powered by the View Transitions API.
"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>
);
}
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.
pnpm dlx shadcn@latest add https://matos-ui.com/r/theme-toggler-button.jsonimport { ThemeTogglerButton } from "@/components/matos-ui/theme-toggler-button"
<ThemeTogglerButton />Requires a ThemeProvider from next-themes (with attribute="class") somewhere above it in the tree.
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.
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"]} />Override any of the icons per mode:
import { CloudMoon, CloudSun } from "lucide-react"
<ThemeTogglerButton
icons={{ light: <CloudSun />, dark: <CloudMoon /> }}
/>| Prop | Type | Default |
|---|---|---|
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"] |
duration | number | 650 |
icons | Partial<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.
Install Matos UI
Choose a package manager and copy one command for every component.