Add dark mode support to your application with smooth transitions and system preference detection.
The
nach-themes library provides a simple and elegant way to implement dark mode in your React applications, with support for system preferences, smooth transitions, and click animations.Installation
Install the package via pnpm:
npm install install nach-themes
Quick Start
1. Configure your CSS
First, ensure you have both light and dark theme variables defined in your CSS:
1:root {2 --background: oklch(99.405% 0.00011 271.152);3 --foreground: oklch(0% 0 0);4 /* ... other light mode variables */5}67.dark {8 --background: oklch(20% 0.02 230);9 --foreground: oklch(96% 0.008 230);10 /* ... other dark mode variables */11}
2. Wrap your app with ThemeProvider
Create a providers component to wrap your application:
1'use client';23import { ThemeProvider } from 'nach-themes';45export function Providers({ children }: { children: React.ReactNode }) {6 return <ThemeProvider>{children}</ThemeProvider>;7}
Then use it in your root layout:
1// app/layout.tsx2import { Providers } from '@/components/providers';34export default function RootLayout({ children }: { children: React.ReactNode }) {5 return (6 <html lang="en" suppressHydrationWarning>7 <body>8 <Providers>{children}</Providers>9 </body>10 </html>11 );12}
3. Create a theme toggle button
1'use client';23import { useTheme } from 'nach-themes';4import { MoonIcon, SunIcon } from '@radix-ui/react-icons';5import { Button } from '@/components/ui/button';67export function ThemeToggle() {8 const { theme, setTheme } = useTheme();910 const isDark = theme === 'dark';11 const Icon = isDark ? MoonIcon : SunIcon;1213 return (14 <Button15 variant="ghost"16 size="icon"17 onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}18 aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}19 >20 <Icon className="h-5 w-5" />21 </Button>22 );23}
Features
System Theme Detection
By default, the theme provider respects the user's system preference:
1<ThemeProvider>{children}</ThemeProvider>
The library automatically detects changes in system preferences and updates the theme accordingly when set to
'system'.Available Themes
The
useTheme hook provides access to three theme modes:'light'- Light mode'dark'- Dark mode'system'- Follows system preference
1const { theme, setTheme, themes } = useTheme();23// themes = ['light', 'dark', 'system']
Smooth View Transitions
The library includes built-in support for the View Transitions API, creating smooth animated transitions between themes. When you pass a click event to
setTheme, it creates a circular reveal animation from the click position:1<button onClick={(e) => setTheme('dark', e)}>Toggle Theme</button>
Without the click event, it falls back to a standard cross-fade transition:
1<button onClick={() => setTheme('dark')}>Toggle Theme</button>
Resolved Theme
Get the actual theme being applied, even when set to
'system':1const { theme, resolvedTheme } = useTheme();23// theme = 'system'4// resolvedTheme = 'dark' (if system prefers dark)
Advanced Usage
Theme Toggle with Loading State
Handle the hydration state to prevent layout shifts:
1'use client';23import { useMounted } from '@/hooks/use-mounted';4import { useTheme } from 'nach-themes';5import { MoonIcon, SunIcon } from '@radix-ui/react-icons';6import { Button } from '@/components/ui/button';78export function ThemeToggle() {9 const mounted = useMounted();10 const { theme, setTheme } = useTheme();1112 if (!mounted) {13 return (14 <Button15 variant="ghost"16 size="icon"17 disabled18 className="bg-muted/30 animate-pulse cursor-default"19 >20 <div className="bg-foreground/20 h-5 w-5 rounded-full" />21 </Button>22 );23 }2425 const isDark = theme === 'dark';26 const Icon = isDark ? MoonIcon : SunIcon;2728 return (29 <Button30 variant="ghost"31 size="icon"32 onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}33 aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}34 className="h-8 w-8"35 >36 <Icon className="h-5 w-5" />37 </Button>38 );39}
The
useMounted hook can be implemented as:1import { useEffect, useState } from 'react';23export function useMounted() {4 const [mounted, setMounted] = useState(false);56 useEffect(() => {7 setMounted(true);8 }, []);910 return mounted;11}
Theme Dropdown
Create a more sophisticated theme selector:
1'use client';23import { useTheme } from 'nach-themes';4import {5 DropdownMenu,6 DropdownMenuContent,7 DropdownMenuItem,8 DropdownMenuTrigger,9} from '@/components/ui/dropdown-menu';10import { Button } from '@/components/ui/button';11import { MoonIcon, SunIcon, DesktopIcon } from '@radix-ui/react-icons';1213export function ThemeDropdown() {14 const { theme, setTheme } = useTheme();1516 return (17 <DropdownMenu>18 <DropdownMenuTrigger asChild>19 <Button variant="ghost" size="icon">20 <SunIcon className="h-5 w-5 scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />21 <MoonIcon className="absolute h-5 w-5 scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />22 <span className="sr-only">Toggle theme</span>23 </Button>24 </DropdownMenuTrigger>25 <DropdownMenuContent align="end">26 <DropdownMenuItem onClick={() => setTheme('light')}>27 <SunIcon className="mr-2 h-4 w-4" />28 Light29 </DropdownMenuItem>30 <DropdownMenuItem onClick={() => setTheme('dark')}>31 <MoonIcon className="mr-2 h-4 w-4" />32 Dark33 </DropdownMenuItem>34 <DropdownMenuItem onClick={() => setTheme('system')}>35 <DesktopIcon className="mr-2 h-4 w-4" />36 System37 </DropdownMenuItem>38 </DropdownMenuContent>39 </DropdownMenu>40 );41}
Programmatic Theme Changes
Access theme information anywhere in your app:
1'use client';23import { useTheme } from 'nach-themes';4import { useEffect } from 'react';56export function DynamicContent() {7 const { resolvedTheme } = useTheme();89 useEffect(() => {10 // Update third-party libraries based on theme11 if (resolvedTheme === 'dark') {12 // Initialize dark mode for external services13 }14 }, [resolvedTheme]);1516 return (17 <div>18 <p>Current theme: {resolvedTheme}</p>19 </div>20 );21}
TypeScript Support
The library is fully typed. All hooks and components have complete TypeScript definitions:
1import { Theme } from 'nach-themes';23const myTheme: Theme = 'dark';