Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions apps/www/content/docs/components/animated-theme-toggler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,22 @@ The preview at the top of this page uses the default **circle** reveal. Below, e

<ComponentPreview name="animated-theme-toggler-star-demo" />

### Next.js (next-themes)

Pass `theme` and `onThemeChange` to keep `useTheme()` subscribers in sync on toggle. When controlled, the component skips `localStorage` and lets the parent own persistence.

<ComponentPreview name="animated-theme-toggler-next-themes-demo" />

## Props

| Prop | Type | Default | Description |
| ------------ | ------------------- | ---------- | ------------------------------------------------------------------------ |
| `className` | `string` | — | Additional classes for the button |
| `duration` | `number` | `400` | Duration of the theme transition animation in milliseconds |
| `variant` | `TransitionVariant` | `"circle"` | Shape used for the view-transition clip-path reveal |
| `fromCenter` | `boolean` | `false` | If true, the clip expands from the viewport center instead of the button |
| Prop | Type | Default | Description |
| --------------- | ------------------------------------ | ---------- | ----------------------------------------------------------------------------------- |
| `className` | `string` | — | Additional classes for the button |
| `duration` | `number` | `400` | Duration of the theme transition animation in milliseconds |
| `variant` | `TransitionVariant` | `"circle"` | Shape used for the view-transition clip-path reveal |
| `fromCenter` | `boolean` | `false` | If true, the clip expands from the viewport center instead of the button |
| `theme` | `"light" \| "dark"` | — | Controlled theme value. When set, the parent owns persistence (e.g. `next-themes`). |
| `onThemeChange` | `(theme: "light" \| "dark") => void` | — | Called on toggle. Pair with `theme` for controlled usage. |

Export `TransitionVariant` from the same module when you need typed props or menus.

Expand Down
55 changes: 49 additions & 6 deletions apps/www/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3091,6 +3091,13 @@ interface AnimatedThemeTogglerProps extends React.ComponentPropsWithoutRef<"butt
variant?: TransitionVariant
/** When true, the transition expands from the viewport center instead of the button center. */
fromCenter?: boolean
/**
* Controlled theme value. When provided, the parent owns persistence
* (e.g. `next-themes`) and this component will not write to localStorage.
*/
theme?: "light" | "dark"
/** Called on toggle. Pair with `theme` for controlled usage. */
onThemeChange?: (theme: "light" | "dark") => void
}

function polygonCollapsed(cx: number, cy: number, vertexCount: number): string {
Expand Down Expand Up @@ -3202,15 +3209,21 @@ export const AnimatedThemeToggler = ({
duration = 400,
variant,
fromCenter = false,
theme,
onThemeChange,
...props
}: AnimatedThemeTogglerProps) => {
const shape = variant ?? "circle"
const [isDark, setIsDark] = useState(false)
const isControlled = theme !== undefined
const [internalIsDark, setInternalIsDark] = useState(false)
const isDark = isControlled ? theme === "dark" : internalIsDark
const buttonRef = useRef<HTMLButtonElement>(null)

useEffect(() => {
if (isControlled) return

const updateTheme = () => {
setIsDark(document.documentElement.classList.contains("dark"))
setInternalIsDark(document.documentElement.classList.contains("dark"))
}

updateTheme()
Expand All @@ -3222,7 +3235,7 @@ export const AnimatedThemeToggler = ({
})

return () => observer.disconnect()
}, [])
}, [isControlled])

const toggleTheme = useCallback(() => {
const button = buttonRef.current
Expand All @@ -3249,9 +3262,15 @@ export const AnimatedThemeToggler = ({

const applyTheme = () => {
const newTheme = !isDark
setIsDark(newTheme)
// Always toggle the class synchronously so the View Transitions API
// snapshots the new theme inside the startViewTransition callback.
document.documentElement.classList.toggle("dark")
localStorage.setItem("theme", newTheme ? "dark" : "light")
if (isControlled) {
onThemeChange?.(newTheme ? "dark" : "light")
} else {
setInternalIsDark(newTheme)
localStorage.setItem("theme", newTheme ? "dark" : "light")
}
}

if (typeof document.startViewTransition !== "function") {
Expand Down Expand Up @@ -3309,7 +3328,7 @@ export const AnimatedThemeToggler = ({
)
})
}
}, [shape, fromCenter, duration, isDark])
}, [shape, fromCenter, duration, isDark, isControlled, onThemeChange])

return (
<button
Expand Down Expand Up @@ -3431,6 +3450,30 @@ export default function AnimatedThemeTogglerStarDemo() {
}


===== EXAMPLE: animated-theme-toggler-next-themes-demo =====
Title: Animated Theme Toggler — next-themes

--- file: example/animated-theme-toggler-next-themes-demo.tsx ---
"use client"

import { useTheme } from "next-themes"

import { AnimatedThemeToggler } from "@/registry/magicui/animated-theme-toggler"

export default function AnimatedThemeTogglerNextThemesDemo() {
const { resolvedTheme, setTheme } = useTheme()

return (
<div className="flex justify-center p-6">
<AnimatedThemeToggler
theme={resolvedTheme === "dark" ? "dark" : "light"}
onThemeChange={setTheme}
/>
</div>
)
}



===== COMPONENT: aurora-text =====
Title: Aurora Text
Expand Down
1 change: 1 addition & 0 deletions apps/www/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Animated Theme Toggler — Hexagon](https://github.com/magicuidesign/magicui/blob/main/example/animated-theme-toggler-hexagon-demo.tsx): Example usage
- [Animated Theme Toggler — Triangle](https://github.com/magicuidesign/magicui/blob/main/example/animated-theme-toggler-triangle-demo.tsx): Example usage
- [Animated Theme Toggler — Star](https://github.com/magicuidesign/magicui/blob/main/example/animated-theme-toggler-star-demo.tsx): Example usage
- [Animated Theme Toggler — next-themes](https://github.com/magicuidesign/magicui/blob/main/example/animated-theme-toggler-next-themes-demo.tsx): Example usage
- [light-rays-demo](https://github.com/magicuidesign/magicui/blob/main/example/light-rays-demo.tsx): Example usage
- [Dotted Map Demo](https://github.com/magicuidesign/magicui/blob/main/example/dotted-map-demo.tsx): Example usage
- [Dotted Map Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/dotted-map-demo-2.tsx): Example usage
Expand Down
20 changes: 20 additions & 0 deletions apps/www/public/r/animated-theme-toggler-next-themes-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "animated-theme-toggler-next-themes-demo",
"type": "registry:example",
"title": "Animated Theme Toggler — next-themes",
"description": "Controlled usage with next-themes so useTheme() subscribers stay in sync on toggle.",
"dependencies": [
"next-themes"
],
"registryDependencies": [
"@magicui/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-next-themes-demo.tsx",
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\n\nimport { AnimatedThemeToggler } from \"@/registry/magicui/animated-theme-toggler\"\n\nexport default function AnimatedThemeTogglerNextThemesDemo() {\n const { resolvedTheme, setTheme } = useTheme()\n\n return (\n <div className=\"flex justify-center p-6\">\n <AnimatedThemeToggler\n theme={resolvedTheme === \"dark\" ? \"dark\" : \"light\"}\n onThemeChange={setTheme}\n />\n </div>\n )\n}\n",
"type": "registry:example"
}
]
}
2 changes: 1 addition & 1 deletion apps/www/public/r/animated-theme-toggler.json

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions apps/www/public/r/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -3754,6 +3754,24 @@
}
]
},
{
"name": "animated-theme-toggler-next-themes-demo",
"type": "registry:example",
"title": "Animated Theme Toggler — next-themes",
"description": "Controlled usage with next-themes so useTheme() subscribers stay in sync on toggle.",
"dependencies": [
"next-themes"
],
"registryDependencies": [
"@magicui/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-next-themes-demo.tsx",
"type": "registry:example"
}
]
},
{
"name": "light-rays-demo",
"type": "registry:example",
Expand Down
18 changes: 18 additions & 0 deletions apps/www/public/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -3754,6 +3754,24 @@
}
]
},
{
"name": "animated-theme-toggler-next-themes-demo",
"type": "registry:example",
"title": "Animated Theme Toggler — next-themes",
"description": "Controlled usage with next-themes so useTheme() subscribers stay in sync on toggle.",
"dependencies": [
"next-themes"
],
"registryDependencies": [
"@magicui/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-next-themes-demo.tsx",
"type": "registry:example"
}
]
},
{
"name": "light-rays-demo",
"type": "registry:example",
Expand Down
18 changes: 18 additions & 0 deletions apps/www/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -3754,6 +3754,24 @@
}
]
},
{
"name": "animated-theme-toggler-next-themes-demo",
"type": "registry:example",
"title": "Animated Theme Toggler — next-themes",
"description": "Controlled usage with next-themes so useTheme() subscribers stay in sync on toggle.",
"dependencies": [
"next-themes"
],
"registryDependencies": [
"@magicui/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-next-themes-demo.tsx",
"type": "registry:example"
}
]
},
{
"name": "light-rays-demo",
"type": "registry:example",
Expand Down
17 changes: 17 additions & 0 deletions apps/www/registry/__index__.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3963,6 +3963,23 @@ export const Index: Record<string, any> = {
}),
meta: undefined,
},
"animated-theme-toggler-next-themes-demo": {
name: "animated-theme-toggler-next-themes-demo",
description: "Controlled usage with next-themes so useTheme() subscribers stay in sync on toggle.",
type: "registry:example",
registryDependencies: ["@magicui/animated-theme-toggler"],
files: [{
path: "registry/example/animated-theme-toggler-next-themes-demo.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/example/animated-theme-toggler-next-themes-demo.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name
return { default: mod.default ?? mod[exportName] }
}),
meta: undefined,
},
"light-rays-demo": {
name: "light-rays-demo",
description: "Demo of the light-rays component showcasing animated light rays",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client"

import { useTheme } from "next-themes"

import { AnimatedThemeToggler } from "@/registry/magicui/animated-theme-toggler"

export default function AnimatedThemeTogglerNextThemesDemo() {
const { resolvedTheme, setTheme } = useTheme()

return (
<div className="flex justify-center p-6">
<AnimatedThemeToggler
theme={resolvedTheme === "dark" ? "dark" : "light"}
onThemeChange={setTheme}
/>
</div>
)
}
31 changes: 25 additions & 6 deletions apps/www/registry/magicui/animated-theme-toggler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ interface AnimatedThemeTogglerProps extends React.ComponentPropsWithoutRef<"butt
variant?: TransitionVariant
/** When true, the transition expands from the viewport center instead of the button center. */
fromCenter?: boolean
/**
* Controlled theme value. When provided, the parent owns persistence
* (e.g. `next-themes`) and this component will not write to localStorage.
*/
theme?: "light" | "dark"
/** Called on toggle. Pair with `theme` for controlled usage. */
onThemeChange?: (theme: "light" | "dark") => void
}

function polygonCollapsed(cx: number, cy: number, vertexCount: number): string {
Expand Down Expand Up @@ -131,15 +138,21 @@ export const AnimatedThemeToggler = ({
duration = 400,
variant,
fromCenter = false,
theme,
onThemeChange,
...props
}: AnimatedThemeTogglerProps) => {
const shape = variant ?? "circle"
const [isDark, setIsDark] = useState(false)
const isControlled = theme !== undefined
const [internalIsDark, setInternalIsDark] = useState(false)
const isDark = isControlled ? theme === "dark" : internalIsDark
const buttonRef = useRef<HTMLButtonElement>(null)

useEffect(() => {
if (isControlled) return

const updateTheme = () => {
setIsDark(document.documentElement.classList.contains("dark"))
setInternalIsDark(document.documentElement.classList.contains("dark"))
}

updateTheme()
Expand All @@ -151,7 +164,7 @@ export const AnimatedThemeToggler = ({
})

return () => observer.disconnect()
}, [])
}, [isControlled])

const toggleTheme = useCallback(() => {
const button = buttonRef.current
Expand All @@ -178,9 +191,15 @@ export const AnimatedThemeToggler = ({

const applyTheme = () => {
const newTheme = !isDark
setIsDark(newTheme)
// Always toggle the class synchronously so the View Transitions API
// snapshots the new theme inside the startViewTransition callback.
document.documentElement.classList.toggle("dark")
localStorage.setItem("theme", newTheme ? "dark" : "light")
if (isControlled) {
onThemeChange?.(newTheme ? "dark" : "light")
} else {
setInternalIsDark(newTheme)
localStorage.setItem("theme", newTheme ? "dark" : "light")
}
}

if (typeof document.startViewTransition !== "function") {
Expand Down Expand Up @@ -238,7 +257,7 @@ export const AnimatedThemeToggler = ({
)
})
}
}, [shape, fromCenter, duration, isDark])
}, [shape, fromCenter, duration, isDark, isControlled, onThemeChange])

return (
<button
Expand Down
15 changes: 15 additions & 0 deletions apps/www/registry/registry-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,21 @@ export const examples: Registry["items"] = [
},
],
},
{
name: "animated-theme-toggler-next-themes-demo",
type: "registry:example",
title: "Animated Theme Toggler — next-themes",
description:
"Controlled usage with next-themes so useTheme() subscribers stay in sync on toggle.",
registryDependencies: ["@magicui/animated-theme-toggler"],
dependencies: ["next-themes"],
files: [
{
path: "example/animated-theme-toggler-next-themes-demo.tsx",
type: "registry:example",
},
],
},
{
name: "light-rays-demo",
description:
Expand Down
Loading