Theme Toggle
A light/dark theme toggle button: View Transitions expand a circle out from the click point, and the Sun/Moon icons rotate through the transition.
Loading preview…
npx shadcn@latest add https://webberui.com/r/theme-toggle.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
220
<ThemeToggle />
Installation
npx shadcn@latest add https://webberui.com/r/theme-toggle.jsonInstalling also writes the --wb-duration-fast and --wb-ease-out CSS variables into your global stylesheet.
Usage
import { ThemeToggle } from "@/components/ui/theme-toggle";
// Uncontrolled: toggles the html dark class automatically and writes localStorage("theme")
<ThemeToggle />
// Controlled: pair it with next-themes or another theme solution
<ThemeToggle
isDark={resolvedTheme === "dark"}
onToggle={(next) => setTheme(next ? "dark" : "light")}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
isDark | boolean | — | Controlled mode: whether the theme is currently dark. When omitted, the component manages html.dark and localStorage itself |
onToggle | (next: boolean) => void | — | Fires on toggle, returning whether it is dark after the toggle. In controlled mode, update your theme state here |
duration | number | 220 | Duration of the circular expansion (milliseconds). During the transition the whole page is a bitmap snapshot and text looks blurry, so it is deliberately kept short |
className | string | — | Extra styles |
How it works
- View Transitions support: Chrome / Edge 111+ and Safari 18+ support
document.startViewTransition; Firefox does not yet. Unsupported browsers simply switch the theme (with no expansion animation), and functionality is completely unaffected - Circular expansion: taking the click coordinates as the center and the distance to the farthest viewport corner as the radius, it runs a
clipPathanimation on::view-transition-new(root); when triggered from the keyboard, the button's center is the origin - Controlled mode: the state update inside
onToggleis applied synchronously withflushSyncin thestartViewTransitioncallback, ensuring the snapshot happens at the right moment - SSR safe: when uncontrolled it only reads
html.darkafter mounting, rendering a neutral icon before that to avoid a hydration mismatch - When the user has "reduce motion" enabled at the system level, it degrades to a direct switch and the icon transition becomes a fade in / fade out
Magnetic Button
A magnetic button that is pulled towards the cursor as it approaches, with a parallax on the inner text and a spring back once the cursor leaves.
Play/Pause Morph
A graphical button that morphs smoothly between a play triangle and pause bars, with controlled and uncontrolled modes and an optional progress ring.