Theme Toggler
magicui/animated-theme-togglerFreeComponent
Theme toggle with View Transitions and animated clip-path masks (circle, polygons, star), optional viewport-centered origin.
Live preview
live · rendered by the registry
Rendered by magicui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://magicui.design/r/animated-theme-toggler.jsonSource
1"use client"23import { useCallback, useEffect, useRef, useState } from "react"4import { Moon, Sun } from "lucide-react"5import { flushSync } from "react-dom"67import { cn } from "@/lib/utils"89export type TransitionVariant =10 | "circle"11 | "square"12 | "triangle"13 | "diamond"14 | "hexagon"15 | "rectangle"16 | "star"1718interface AnimatedThemeTogglerProps extends React.ComponentPropsWithoutRef<"button"> {19 duration?: number20 variant?: TransitionVariant21 /** When true, the transition expands from the viewport center instead of the button center. */22 fromCenter?: boolean23 /**24 * Controlled theme value. When provided, the parent owns persistence25 * (e.g. `next-themes`) and this component will not write to localStorage.26 */27 theme?: "light" | "dark"28 /** Called on toggle. Pair with `theme` for controlled usage. */29 onThemeChange?: (theme: "light" | "dark") => void30}3132function polygonCollapsed(point: string, vertexCount: number): string {33 const pairs = Array.from({ length: vertexCount }, () => point).join(", ")34 return `polygon(${pairs})`35}3637// All coordinates are percentages of the snapshot reference box: Chrome 15038// renders absolute px clip-path coordinates on ::view-transition-new(root)39// unscaled on fractional display scales (e.g. Windows 150%) for the first40// transition after load, so px values land at the wrong position (#989).41function getThemeTransitionClipPaths(42 variant: TransitionVariant,43 cx: number,44 cy: number,45 maxRadius: number,46 viewportWidth: number,47 viewportHeight: number48): [string, string] {49 const toX = (x: number) => `${(x / viewportWidth) * 100}%`50 const toY = (y: number) => `${(y / viewportHeight) * 100}%`51 const point = (x: number, y: number) => `${toX(x)} ${toY(y)}`52 // circle() percentage radii resolve against hypot(w, h) / sqrt(2) of the reference box.53 const toRadius = (r: number) =>54 `${(r / (Math.hypot(viewportWidth, viewportHeight) / Math.SQRT2)) * 100}%`5556 switch (variant) {57 case "circle":58 return [59 `circle(0% at ${point(cx, cy)})`,60 `circle(${toRadius(maxRadius)} at ${point(cx, cy)})`,61 ]62 case "square": {63 const halfW = Math.max(cx, viewportWidth - cx)64 const halfH = Math.max(cy, viewportHeight - cy)65 const halfSide = Math.max(halfW, halfH) * 1.0566 const end = [67 point(cx - halfSide, cy - halfSide),68 point(cx + halfSide, cy - halfSide),69 point(cx + halfSide, cy + halfSide),70// … truncated
What it pulls in
npm packages
Files it writes
More from magicui
All 247 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Androidandroid | magicui | Components | Free | no deps | |
| Animated Beamanimated-beam | magicui | Components | Free | 1 dep | |
| Animated Circular Progress Baranimated-circular-progress-bar | magicui | Components | Free | no deps | |
| Animated Gradient Textanimated-gradient-text | magicui | Components | Free | no deps | |
| Animated Grid Patternanimated-grid-pattern | magicui | Components | Free | 1 dep | |
| Animated Listanimated-list | magicui | Components | Free | 1 dep | |
| Animated Shiny Textanimated-shiny-text | magicui | Components | Free | no deps | |
| Aurora Textaurora-text | magicui | Components | Free | no deps |
