Dark Mode

Add dark mode support to your application with smooth transitions and system preference detection.

The i7a-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:

pnpm add i7a-themes

Quick Start

1. Configure your CSS

First, ensure you have both light and dark theme variables defined in your CSS:

:root {
--background: oklch(99.405% 0.00011 271.152);
--foreground: oklch(0% 0 0);
/* ... other light mode variables */
}
.dark {
--background: oklch(20% 0.02 230);
--foreground: oklch(96% 0.008 230);
/* ... other dark mode variables */
}

2. Wrap your app with ThemeProvider

Create a providers component to wrap your application:

'use client';
import { ThemeProvider } from 'i7a-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}

Then use it in your root layout:

// app/layout.tsx
import { Providers } from '@/components/providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

3. Create a theme toggle button

'use client';
import { useTheme } from 'i7a-themes';
import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const isDark = theme === 'dark';
const Icon = isDark ? MoonIcon : SunIcon;
return (
<Button
variant="ghost"
size="icon"
onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
>
<Icon className="h-5 w-5" />
</Button>
);
}

Features

System Theme Detection

By default, the theme provider respects the user's system preference:

<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
const { theme, setTheme, themes } = useTheme();
// 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:

<button onClick={(e) => setTheme('dark', e)}>Toggle Theme</button>

Without the click event, it falls back to a standard cross-fade transition:

<button onClick={() => setTheme('dark')}>Toggle Theme</button>

Resolved Theme

Get the actual theme being applied, even when set to 'system':

const { theme, resolvedTheme } = useTheme();
// theme = 'system'
// resolvedTheme = 'dark' (if system prefers dark)

Advanced Usage

Theme Toggle with Loading State

Handle the hydration state to prevent layout shifts:

'use client';
import { useMounted } from '@/hooks/use-mounted';
import { useTheme } from 'i7a-themes';
import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const mounted = useMounted();
const { theme, setTheme } = useTheme();
if (!mounted) {
return (
<Button
variant="ghost"
size="icon"
disabled
className="bg-muted/30 animate-pulse cursor-default"
>
<div className="bg-foreground/20 h-5 w-5 rounded-full" />
</Button>
);
}
const isDark = theme === 'dark';
const Icon = isDark ? MoonIcon : SunIcon;
return (
<Button
variant="ghost"
size="icon"
onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
className="h-8 w-8"
>
<Icon className="h-5 w-5" />
</Button>
);
}

The useMounted hook can be implemented as:

import { useEffect, useState } from 'react';
export function useMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}

Theme Dropdown

Create a more sophisticated theme selector:

'use client';
import { useTheme } from 'i7a-themes';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { MoonIcon, SunIcon, DesktopIcon } from '@radix-ui/react-icons';
export function ThemeDropdown() {
const { theme, setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<SunIcon className="h-5 w-5 scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
<MoonIcon className="absolute h-5 w-5 scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
<SunIcon className="mr-2 h-4 w-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<MoonIcon className="mr-2 h-4 w-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<DesktopIcon className="mr-2 h-4 w-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

Programmatic Theme Changes

Access theme information anywhere in your app:

'use client';
import { useTheme } from 'i7a-themes';
import { useEffect } from 'react';
export function DynamicContent() {
const { resolvedTheme } = useTheme();
useEffect(() => {
// Update third-party libraries based on theme
if (resolvedTheme === 'dark') {
// Initialize dark mode for external services
}
}, [resolvedTheme]);
return (
<div>
<p>Current theme: {resolvedTheme}</p>
</div>
);
}

TypeScript Support

The library is fully typed. All hooks and components have complete TypeScript definitions:

import { Theme } from 'i7a-themes';
const myTheme: Theme = 'dark';
Dark Mode - I7A UI