Skip to content

Animated theme toggler #643

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
48 changes: 48 additions & 0 deletions __registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,29 @@ export const Index: Record<string, any> = {
}),
meta: undefined,
},
"animated-theme-toggler": {
name: "animated-theme-toggler",
description: "A component for theme changing animation.",
type: "registry:ui",
registryDependencies: undefined,
files: [
{
path: "registry/magicui/animated-theme-toggler.tsx",
type: "registry:ui",
target: "components/magicui/animated-theme-toggler.tsx",
},
],
component: React.lazy(async () => {
const mod = await import("@/registry/magicui/animated-theme-toggler.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,
},
"magic-card-demo": {
name: "magic-card-demo",
description:
Expand Down Expand Up @@ -4325,6 +4348,31 @@ export const Index: Record<string, any> = {
}),
meta: undefined,
},
"animated-theme-toggler-demo": {
name: "animated-theme-toggler-demo",
description: "Example showing animation while changing the theme.",
type: "registry:example",
registryDependencies: ["https://magicui.design/animated-theme-toggler"],
files: [
{
path: "registry/example/animated-theme-toggler-demo.tsx",
type: "registry:example",
target: "",
},
],
component: React.lazy(async () => {
const mod = await import(
"@/registry/example/animated-theme-toggler-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,
},
utils: {
name: "utils",
description: "",
Expand Down
6 changes: 6 additions & 0 deletions config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ export const docsConfig: DocsConfig = {
items: [],
label: "",
},
{
title: "Animated Theme Toggler",
href: `/docs/components/animated-theme-toggler`,
items: [],
label: "New",
},
{
title: "File Tree",
href: `/docs/components/file-tree`,
Expand Down
71 changes: 71 additions & 0 deletions content/docs/components/animated-theme-toggler.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Theme Toggler
date: 2025-0-17
description: An Animated theme toggler component, fully customizable using Tailwind CSS.
author: Nazam Kalsi
published: true
---

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

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add "https://magicui.design/r/animated-theme-toggler"
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="animated-theme-toggler" />

<Step>Update the import paths to match your project setup.</Step>

<Step>Add the required CSS into your index.css</Step>

```css title="./index.css"
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
```

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx
import ThemeToggler from "@/components/magicui/animated-theme-toggler";
```

```tsx
<ThemeToggler />
```

## Props

| Prop | Type | Default | Description |
| ----------- | -------- | ------- | ----------------------------------------------- |
| `className` | `String` | " " | Additional classes to be added to the component |

## Credits

- Credit to [Nazam Kalsi](https://nazam-kalsi-portfolio.vercel.app)
16 changes: 16 additions & 0 deletions public/r/animated-theme-toggler-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "animated-theme-toggler-demo",
"type": "registry:example",
"description": "Example showing animation while changing the theme.",
"registryDependencies": [
"https://magicui.design/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-demo.tsx",
"content": "import ThemeToggler from \"@/registry/magicui/animated-theme-toggler\";\n\nfunction AnimatedThemeTogglerDemo() {\n return (\n <div>\n <ThemeToggler />\n </div>\n );\n}\nexport default AnimatedThemeTogglerDemo;\n",
"type": "registry:example"
}
]
}
24 changes: 24 additions & 0 deletions public/r/animated-theme-toggler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "animated-theme-toggler",
"type": "registry:ui",
"title": "Theme Toggler",
"description": "A component for theme changing animation.",
"dependencies": [
"lucide-react"
],
"files": [
{
"path": "registry/magicui/animated-theme-toggler.tsx",
"content": "\"use client\";\n\nimport { Moon, SunDim } from \"lucide-react\";\nimport { useState, useRef } from \"react\";\nimport { flushSync } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\n\ntype props = {\n className?: string;\n};\n\nconst ThemeToggler = ({ className }: props) => {\n const [isDarkMode, setIsDarkMode] = useState<boolean>(false);\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n const changeTheme = async () => {\n if (!buttonRef.current) return;\n\n await document.startViewTransition(() => {\n flushSync(() => {\n const dark = document.documentElement.classList.toggle(\"dark\");\n setIsDarkMode(dark);\n });\n }).ready;\n\n const { top, left, width, height } =\n buttonRef.current.getBoundingClientRect();\n const y = top + height / 2;\n const x = left + width / 2;\n\n const right = window.innerWidth - left;\n const bottom = window.innerHeight - top;\n const maxRad = Math.hypot(Math.max(left, right), Math.max(top, bottom));\n\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${maxRad}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration: 700,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n },\n );\n };\n return (\n <button ref={buttonRef} onClick={changeTheme} className={cn(className)}>\n {isDarkMode ? <SunDim /> : <Moon />}\n </button>\n );\n};\nexport default ThemeToggler;\n",
"type": "registry:ui",
"target": "components/magicui/animated-theme-toggler.tsx"
}
],
"css": {
"::view-transition-old(root), ::view-transition-new(root)": {
"animation": "none",
"mix-blend-mode": "normal"
}
}
}
16 changes: 16 additions & 0 deletions public/r/theme-toggler-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "theme-toggler-demo",
"type": "registry:example",
"description": "Example showing animation in changing the theme.",
"registryDependencies": [
"animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-demo.tsx",
"content": "import ThemeToggler from '@/registry/magicui/animated-theme-toggler';\r\n\r\nfunction AnimatedThemeTogglerDemo() {\r\n return (\r\n <div className='flex justify-center items-center'>\r\n <ThemeToggler/>\r\n </div>\r\n )\r\n}\r\nexport default AnimatedThemeTogglerDemo;",
"type": "registry:example"
}
]
}
36 changes: 36 additions & 0 deletions public/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,28 @@
}
]
},
{
"name": "animated-theme-toggler",
"type": "registry:ui",
"title": "Theme Toggler",
"description": "A component for theme changing animation.",
"dependencies": [
"lucide-react"
],
"files": [
{
"path": "registry/magicui/animated-theme-toggler.tsx",
"type": "registry:ui",
"target": "components/magicui/animated-theme-toggler.tsx"
}
],
"css": {
"::view-transition-old(root), ::view-transition-new(root)": {
"animation": "none",
"mix-blend-mode": "normal"
}
}
},
{
"name": "magic-card-demo",
"type": "registry:example",
Expand Down Expand Up @@ -3163,6 +3185,20 @@
}
]
},
{
"name": "animated-theme-toggler-demo",
"type": "registry:example",
"description": "Example showing animation while changing the theme.",
"registryDependencies": [
"https://magicui.design/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-demo.tsx",
"type": "registry:example"
}
]
},
{
"name": "utils",
"type": "registry:lib",
Expand Down
36 changes: 36 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,28 @@
}
]
},
{
"name": "animated-theme-toggler",
"type": "registry:ui",
"title": "Theme Toggler",
"description": "A component for theme changing animation.",
"dependencies": [
"lucide-react"
],
"files": [
{
"path": "registry/magicui/animated-theme-toggler.tsx",
"type": "registry:ui",
"target": "components/magicui/animated-theme-toggler.tsx"
}
],
"css": {
"::view-transition-old(root), ::view-transition-new(root)": {
"animation": "none",
"mix-blend-mode": "normal"
}
}
},
{
"name": "magic-card-demo",
"type": "registry:example",
Expand Down Expand Up @@ -3163,6 +3185,20 @@
}
]
},
{
"name": "animated-theme-toggler-demo",
"type": "registry:example",
"description": "Example showing animation while changing the theme.",
"registryDependencies": [
"https://magicui.design/animated-theme-toggler"
],
"files": [
{
"path": "registry/example/animated-theme-toggler-demo.tsx",
"type": "registry:example"
}
]
},
{
"name": "utils",
"type": "registry:lib",
Expand Down
10 changes: 10 additions & 0 deletions registry/example/animated-theme-toggler-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ThemeToggler from "@/registry/magicui/animated-theme-toggler";

function AnimatedThemeTogglerDemo() {
return (
<div>
<ThemeToggler />
</div>
);
}
export default AnimatedThemeTogglerDemo;
54 changes: 54 additions & 0 deletions registry/magicui/animated-theme-toggler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import { Moon, SunDim } from "lucide-react";
import { useState, useRef } from "react";
import { flushSync } from "react-dom";
import { cn } from "@/lib/utils";

type props = {
className?: string;
};

const ThemeToggler = ({ className }: props) => {
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const changeTheme = async () => {
if (!buttonRef.current) return;

await document.startViewTransition(() => {
flushSync(() => {
const dark = document.documentElement.classList.toggle("dark");
setIsDarkMode(dark);
});
}).ready;

const { top, left, width, height } =
buttonRef.current.getBoundingClientRect();
const y = top + height / 2;
const x = left + width / 2;

const right = window.innerWidth - left;
const bottom = window.innerHeight - top;
const maxRad = Math.hypot(Math.max(left, right), Math.max(top, bottom));

document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${maxRad}px at ${x}px ${y}px)`,
],
},
{
duration: 700,
easing: "ease-in-out",
pseudoElement: "::view-transition-new(root)",
},
);
};
return (
<button ref={buttonRef} onClick={changeTheme} className={cn(className)}>
{isDarkMode ? <SunDim /> : <Moon />}
</button>
);
};
export default ThemeToggler;
12 changes: 12 additions & 0 deletions registry/registry-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1696,4 +1696,16 @@ export const examples: Registry["items"] = [
},
],
},
{
name: "animated-theme-toggler-demo",
description: "Example showing animation while changing the theme.",
type: "registry:example",
registryDependencies: ["https://magicui.design/animated-theme-toggler"],
files: [
{
path: "registry/example/animated-theme-toggler-demo.tsx",
type: "registry:example",
},
],
},
];
Loading