Theme Toggle

Bouton pour basculer entre les themes clair et sombre. Installez Theme Toggle depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

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.json

Source

"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.

Dependances

  • @vllnt/ui@^0.3.0