WebberUI

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.json

Playground

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.json

Installing 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

PropTypeDefaultDescription
isDarkbooleanControlled mode: whether the theme is currently dark. When omitted, the component manages html.dark and localStorage itself
onToggle(next: boolean) => voidFires on toggle, returning whether it is dark after the toggle. In controlled mode, update your theme state here
durationnumber220Duration 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
classNamestringExtra 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 clipPath animation on ::view-transition-new(root); when triggered from the keyboard, the button's center is the origin
  • Controlled mode: the state update inside onToggle is applied synchronously with flushSync in the startViewTransition callback, ensuring the snapshot happens at the right moment
  • SSR safe: when uncontrolled it only reads html.dark after 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

On this page