Skip to main content

Dark Mode

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}
6
7.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';
2
3import { ThemeProvider } from 'nach-themes';
4
5export function Providers({ children }: { children: React.ReactNode }) {
6 return <ThemeProvider>{children}</ThemeProvider>;
7}
Then use it in your root layout:
1// app/layout.tsx
2import { Providers } from '@/components/providers';
3
4export 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';
2
3import { useTheme } from 'nach-themes';
4import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
5import { Button } from '@/components/ui/button';
6
7export function ThemeToggle() {
8 const { theme, setTheme } = useTheme();
9
10 const isDark = theme === 'dark';
11 const Icon = isDark ? MoonIcon : SunIcon;
12
13 return (
14 <Button
15 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();
2
3// 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();
2
3// 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';
2
3import { useMounted } from '@/hooks/use-mounted';
4import { useTheme } from 'nach-themes';
5import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
6import { Button } from '@/components/ui/button';
7
8export function ThemeToggle() {
9 const mounted = useMounted();
10 const { theme, setTheme } = useTheme();
11
12 if (!mounted) {
13 return (
14 <Button
15 variant="ghost"
16 size="icon"
17 disabled
18 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 }
24
25 const isDark = theme === 'dark';
26 const Icon = isDark ? MoonIcon : SunIcon;
27
28 return (
29 <Button
30 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';
2
3export function useMounted() {
4 const [mounted, setMounted] = useState(false);
5
6 useEffect(() => {
7 setMounted(true);
8 }, []);
9
10 return mounted;
11}

Theme Dropdown

Create a more sophisticated theme selector:
1'use client';
2
3import { 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';
12
13export function ThemeDropdown() {
14 const { theme, setTheme } = useTheme();
15
16 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 Light
29 </DropdownMenuItem>
30 <DropdownMenuItem onClick={() => setTheme('dark')}>
31 <MoonIcon className="mr-2 h-4 w-4" />
32 Dark
33 </DropdownMenuItem>
34 <DropdownMenuItem onClick={() => setTheme('system')}>
35 <DesktopIcon className="mr-2 h-4 w-4" />
36 System
37 </DropdownMenuItem>
38 </DropdownMenuContent>
39 </DropdownMenu>
40 );
41}

Programmatic Theme Changes

Access theme information anywhere in your app:
1'use client';
2
3import { useTheme } from 'nach-themes';
4import { useEffect } from 'react';
5
6export function DynamicContent() {
7 const { resolvedTheme } = useTheme();
8
9 useEffect(() => {
10 // Update third-party libraries based on theme
11 if (resolvedTheme === 'dark') {
12 // Initialize dark mode for external services
13 }
14 }, [resolvedTheme]);
15
16 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';
2
3const myTheme: Theme = 'dark';