Theme Toggle
Bouton pour basculer entre les themes clair et sombre. Installez Theme Toggle depuis le registre VLLNT UI avec la CLI shadcn.
Bouton pour basculer entre les themes clair et sombre. Fait partie de la famille utility dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Theme Toggle a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/theme-toggle.jsonSource
"use client";
import { useCallback, useRef, useState } from "react";
import { useTheme } from "next-themes";
import { useMounted } from "../../lib/use-mounted";
import { Button } from "../button/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../dropdown-menu/dropdown-menu";
type ThemeToggleProps = {
dict: {
theme: {
dark: string;
light: string;
system: string;
toggle_theme: string;
};
};
};
type ThemeButtonProps = {
ariaLabel: string;
children?: React.ReactNode;
icon: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
function ThemeButton({
ariaLabel,
children,
icon,
...props
}: ThemeButtonProps) {
return (
<Button
aria-label={ariaLabel}
className="bg-background text-foreground border border-zinc-300 dark:border-zinc-600 hover:border-zinc-400 dark:hover:border-zinc-500 transition-colors"
size="icon"
variant="outline"
{...props}
>
<span className="text-sm font-mono">{icon}</span>
<span className="sr-only">{ariaLabel}</span>
{children}
</Button>
);
}
function ThemeMenuItem({
icon,
label,
onClick,
}: {
icon: string;
label: string;
onClick: () => void;
}) {
return (
<DropdownMenuItem
className="hover:bg-accent cursor-pointer"
onClick={onClick}
>
{icon} {label}
</DropdownMenuItem>
);
}
export function ThemeToggle({ dict }: ThemeToggleProps) {
const { setTheme, theme } = useTheme();
const mounted = useMounted();
const [open, setOpen] = useState(false);
const closeTimerReference = useRef<null | number>(null);
const isHoveringOverMenuAreaReference = useRef(false);
const clearCloseTimer = useCallback(() => {
if (closeTimerReference.current !== null) {
window.clearTimeout(closeTimerReference.current);
closeTimerReference.current = null;
}
}, []);
const scheduleClose = useCallback(() => {
clearCloseTimer();
closeTimerReference.current = window.setTimeout(() => {
setOpen(false);
}, 250);
}, [clearCloseTimer]);
const handleOpenChange = useCallback((nextOpen: boolean) => {
if (!nextOpen && isHoveringOverMenuAreaReference.current) {
return;
}
setOpen(nextOpen);
}, []);
const getThemeIcon = useCallback(() => {
if (!mounted) return "☀";
switch (theme) {
case "light":
return "☀";
case "dark":
return "☾";
case "system":
return "⚙";
case undefined:
return "⚙";
default:
return "⚙";
}
}, [theme, mounted]);
const themeIcon = getThemeIcon();
const handleThemeChange = useCallback(
(newTheme: string) => {
setTheme(newTheme);
},
[setTheme],
);
// Prevent hydration mismatch by not rendering until mounted
if (!mounted) {
return <ThemeButton ariaLabel={dict.theme.toggle_theme} icon="☀" />;
}
return (
<DropdownMenu modal={false} onOpenChange={handleOpenChange} open={open}>
<DropdownMenuTrigger asChild>
<ThemeButton
ariaLabel={dict.theme.toggle_theme}
icon={themeIcon}
onMouseEnter={() => {
isHoveringOverMenuAreaReference.current = true;
clearCloseTimer();
setOpen(true);
}}
onMouseLeave={() => {
isHoveringOverMenuAreaReference.current = false;
scheduleClose();
}}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-background border border-zinc-300 dark:border-zinc-600"
onMouseEnter={() => {
isHoveringOverMenuAreaReference.current = true;
clearCloseTimer();
}}
onMouseLeave={() => {
isHoveringOverMenuAreaReference.current = false;
scheduleClose();
}}
>
<ThemeMenuItem
icon="☀"
label={dict.theme.light}
onClick={() => {
handleThemeChange("light");
}}
/>
<ThemeMenuItem
icon="☾"
label={dict.theme.dark}
onClick={() => {
handleThemeChange("dark");
}}
/>
<ThemeMenuItem
icon="⚙"
label={dict.theme.system}
onClick={() => {
handleThemeChange("system");
}}
/>
</DropdownMenuContent>
</DropdownMenu>
);
}
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.