Sections
Get Started
UI
Components
- Animated Beam
- Animated List
- Animated Logo
- Animated Theme Toggler
- Avatar Circles
- Bento Grid
- Big Name Text
- Border Beam
- Dock
- Feature Card
- Filter Dropdown
- Footer Link
- Hero Badge
- Hero Buttons
- Hyper Text
- Lens
- Magic Card
- Marquee
- Meteors
- Onboarding Form
- Pricing Card
- Scroll Text
- Section Header
- Site Header
- Status Card
- Status
- Tweet Card
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import {
ThemeToggleButton,
useThemeTransition,
} from "@/registry/sase/animated-theme-toggler";
export function ThemeToggleVariantsDemo() {
const { theme, setTheme, resolvedTheme } = useTheme();
const { startTransition } = useThemeTransition();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const handleThemeToggle = useCallback(() => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
startTransition(() => {
setTheme(newTheme);
});
}, [resolvedTheme, setTheme, startTransition]);
if (!mounted) {
return null;
}
return (
<div className="flex items-center justify-center gap-8 p-8 flex-wrap">
{/* Circle animation */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle"
start="center"
speed={2000}
/>
<span className="text-xs font-medium">Circle</span>
</div>
{/* Circle blur animation */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle-blur"
start="top-left"
speed={2000}
/>
<span className="text-xs font-medium">Circle Blur</span>
</div>
{/* Polygon animation */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="polygon"
speed={2000}
/>
<span className="text-xs font-medium">Polygon</span>
</div>
{/* GIF animation */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="gif"
url="https://media.tenor.com/qiwdgiFHfBsAAAAj/agenturleben-agencylife.gif"
/>
<span className="text-xs font-medium">GIF Mask</span>
</div>
</div>
);
}
Installation
pnpm dlx shadcn@latest add https://sase-ui.vercel.app/r/animated-theme-toggler.json
Usage
import { ThemeToggleButton, useThemeTransition } from "@/components/ui/animated-theme-toggler"
import { useTheme } from "next-themes"
export function ThemeToggle() {
const { setTheme, resolvedTheme } = useTheme()
const { startTransition } = useThemeTransition()
const toggleTheme = () => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark"
startTransition(() => setTheme(newTheme))
}
return (
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={toggleTheme}
/>
)
}Examples
Styled Variations
You can easily customize the appearance using className and variants:
// Circle variant (default)
<ThemeToggleButton variant="circle" />
// Circle Blur variant
<ThemeToggleButton variant="circle-blur" />
// Polygon variant
<ThemeToggleButton variant="polygon" />
// GIF variant
<ThemeToggleButton
variant="gif"
url="https://media.giphy.com/media/KBbr4hHl9DSahKvInO/giphy.gif"
/>
// With label
<ThemeToggleButton showLabel={true} />
// Custom start position
<ThemeToggleButton start="top-right" />
// Custom speed (slow)
<ThemeToggleButton speed={1000} />GIF Mask Animations
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import {
ThemeToggleButton,
useThemeTransition,
} from "@/registry/sase/animated-theme-toggler";
export function ThemeToggleGifDemo() {
const { setTheme, resolvedTheme } = useTheme();
const { startTransition } = useThemeTransition();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const handleThemeToggle = useCallback(() => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
startTransition(() => {
setTheme(newTheme);
});
}, [resolvedTheme, setTheme, startTransition]);
if (!mounted) {
return null;
}
return (
<div className="flex items-center justify-center gap-4 p-8 flex-wrap">
{/* GIF 1 */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="gif"
url="https://media.tenor.com/qiwdgiFHfBsAAAAj/agenturleben-agencylife.gif"
/>
<span className="text-xs text-muted-foreground">GIT 1</span>
</div>
{/* GIF 2 */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="gif"
url="https://media.giphy.com/media/5PncuvcXbBuIZcSiQo/giphy.gif?cid=ecf05e47j7vdjtytp3fu84rslaivdun4zvfhej6wlvl6qqsz&ep=v1_stickers_search&rid=giphy.gif&ct=s"
/>
<span className="text-xs text-muted-foreground">GIF 2</span>
</div>
{/* GIF 3 */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="gif"
url="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3JwcXdzcHd5MW92NWprZXVpcTBtNXM5cG9obWh0N3I4NzFpaDE3byZlcD12MV9zdGlja2Vyc19zZWFyY2gmY3Q9cw/WgsVx6C4N8tjy/giphy.gif"
/>
<span className="text-xs text-muted-foreground">GIF 3</span>
</div>
{/* GIF 4 */}
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="gif"
url="https://media.giphy.com/media/ArfrRmFCzYXsC6etQX/giphy.gif?cid=ecf05e47kn81xmnuc9vd5g6p5xyjt14zzd3dzwso6iwgpvy3&ep=v1_stickers_search&rid=giphy.gif&ct=s"
/>
<span className="text-xs text-muted-foreground">GIF 4</span>
</div>
<div className="flex flex-col items-center gap-3">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="gif"
url="https://media.giphy.com/media/KBbr4hHl9DSahKvInO/giphy.gif?cid=790b76112m5eeeydoe7et0cr3j3ekb1erunxozyshuhxx2vl&ep=v1_stickers_search&rid=giphy.gif&ct=s"
/>
<span className="text-xs text-muted-foreground">GIF 5</span>
</div>
</div>
);
}
Animation Positions
Circle animations can start from different positions for directional effects:
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import {
ThemeToggleButton,
useThemeTransition,
} from "@/registry/sase/animated-theme-toggler";
export function ThemeTogglePositionsDemo() {
const { setTheme, resolvedTheme } = useTheme();
const { startTransition } = useThemeTransition();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const handleThemeToggle = useCallback(() => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
startTransition(() => {
setTheme(newTheme);
});
}, [resolvedTheme, setTheme, startTransition]);
if (!mounted) {
return null;
}
return (
<div className="flex items-center justify-center gap-12 p-8 flex-wrap">
<div className="flex flex-col items-center gap-2">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle"
start="center"
/>
<span className="text-xs text-muted-foreground">Center</span>
</div>
<div className="flex flex-col items-center gap-2">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle"
start="top-left"
/>
<span className="text-xs text-muted-foreground">Top Left</span>
</div>
<div className="flex flex-col items-center gap-2">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle"
start="top-right"
/>
<span className="text-xs text-muted-foreground">Top Right</span>
</div>
<div className="flex flex-col items-center gap-2">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle"
start="bottom-left"
/>
<span className="text-xs text-muted-foreground">Bottom Left</span>
</div>
<div className="flex flex-col items-center gap-2">
<ThemeToggleButton
theme={resolvedTheme === "dark" ? "dark" : "light"}
onClick={handleThemeToggle}
variant="circle"
start="bottom-right"
/>
<span className="text-xs text-muted-foreground">Bottom Right</span>
</div>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
theme | string | "light" | Current theme value ("light" or "dark") |
onClick | func | - | Callback function called when button is clicked |
showLabel | boolean | false | Whether to show the text label |
className | string | - | Additional classes to be added to the component |
variant | string | "circle" | Animation variant: circle, circle-blur, gif, polygon |
start | string | "center" | Start position: center, top-left, top-right, bottom-left, bottom-right |
speed | number | 500 | Animation duration in milliseconds |
url | string | - | URL for the GIF mask (required when variant is gif) |
Credits
- Credit to Nazam Kalsi