Dark Mode
Nim UI has first-class dark mode support built into every component. Dark mode is implemented using the Tailwind CSS dark: variant with a class-based strategy, giving you full control over when and how dark mode is applied.
Enabling Dark Mode
Dark mode is activated by adding the dark class to your <html> element. When this class is present, all Nim UI components automatically switch to their dark color scheme.
<!-- Light mode (default) --><html> <body> <App /> </body></html>
<!-- Dark mode --><html class="dark"> <body> <App /> </body></html>Implementation Approaches
Manual Toggle
The simplest approach is a toggle button that adds or removes the dark class.
import { useState, useEffect } from 'react';import { Button } from '@nim-ui/components';
function ThemeToggle() { const [isDark, setIsDark] = useState(false);
useEffect(() => { if (isDark) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }, [isDark]);
return ( <Button variant="ghost" onClick={() => setIsDark(!isDark)} aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'} > {isDark ? 'Light Mode' : 'Dark Mode'} </Button> );}System Preference Detection
Detect the user’s operating system theme preference using the prefers-color-scheme media query.
import { useState, useEffect } from 'react';
function useSystemTheme() { const [isDark, setIsDark] = useState(() => { if (typeof window === 'undefined') return false; return window.matchMedia('(prefers-color-scheme: dark)').matches; });
useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => { setIsDark(e.matches); };
mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); }, []);
useEffect(() => { if (isDark) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }, [isDark]);
return isDark;}Persistent Theme with localStorage
Save the user’s preference so it persists across sessions.
import { useState, useEffect } from 'react';
type Theme = 'light' | 'dark' | 'system';
function useTheme() { const [theme, setTheme] = useState<Theme>(() => { if (typeof window === 'undefined') return 'system'; return (localStorage.getItem('theme') as Theme) || 'system'; });
useEffect(() => { const root = document.documentElement;
const applyTheme = () => { if (theme === 'dark') { root.classList.add('dark'); } else if (theme === 'light') { root.classList.remove('dark'); } else { // System preference const prefersDark = window.matchMedia( '(prefers-color-scheme: dark)' ).matches; root.classList.toggle('dark', prefersDark); } };
applyTheme(); localStorage.setItem('theme', theme);
// Listen for system changes when in "system" mode if (theme === 'system') { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handler = () => applyTheme(); mediaQuery.addEventListener('change', handler); return () => mediaQuery.removeEventListener('change', handler); } }, [theme]);
return { theme, setTheme };}Preventing Flash of Incorrect Theme
Add a script in the <head> to set the theme before the page renders. This prevents a flash of the wrong theme on page load.
<head> <script> (function() { const theme = localStorage.getItem('theme'); if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } })(); </script></head>Component Dark Mode Support
All Nim UI components automatically adapt to dark mode. Here is how the key color mappings work:
Backgrounds
| Element | Light | Dark |
|---|---|---|
| Page background | bg-white | dark:bg-neutral-950 |
| Card background | bg-white | dark:bg-neutral-900 |
| Input background | bg-white | dark:bg-neutral-800 |
| Hover background | bg-neutral-50 | dark:bg-neutral-800 |
Text
| Element | Light | Dark |
|---|---|---|
| Primary text | text-neutral-900 | dark:text-neutral-100 |
| Secondary text | text-neutral-600 | dark:text-neutral-400 |
| Muted text | text-neutral-400 | dark:text-neutral-500 |
| Link text | text-primary-600 | dark:text-primary-400 |
Borders
| Element | Light | Dark |
|---|---|---|
| Default border | border-neutral-200 | dark:border-neutral-800 |
| Input border | border-neutral-300 | dark:border-neutral-700 |
| Divider | divide-neutral-200 | dark:divide-neutral-800 |
Using Dark Mode in Custom Components
When building your own components alongside Nim UI, follow the same pattern of adding dark: variants for each color utility.
Custom Component with Dark Mode
Custom Card
This card adapts its colors based on the current theme mode.
Tag
Label
View Code
<div className="rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6"> <h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2"> Custom Card </h3> <p className="text-neutral-600 dark:text-neutral-400 mb-4"> This card adapts its colors based on the current theme mode. </p> <div className="flex gap-2"> <span className="px-2 py-1 text-xs rounded bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300"> Tag </span> </div></div>Theme Context Provider
For larger applications, create a React context to provide theme state to all components.
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextValue { theme: Theme; setTheme: (theme: Theme) => void; resolvedTheme: 'light' | 'dark';}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>(() => { if (typeof window === 'undefined') return 'system'; return (localStorage.getItem('theme') as Theme) || 'system'; });
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
useEffect(() => { const root = document.documentElement; let isDark: boolean;
if (theme === 'system') { isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; } else { isDark = theme === 'dark'; }
root.classList.toggle('dark', isDark); setResolvedTheme(isDark ? 'dark' : 'light'); localStorage.setItem('theme', theme); }, [theme]);
return ( <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}> {children} </ThemeContext.Provider> );}
export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context;}Usage:
import { ThemeProvider, useTheme } from './ThemeProvider';import { Button } from '@nim-ui/components';
function App() { return ( <ThemeProvider> <Navigation /> <MainContent /> </ThemeProvider> );}
function ThemeToggle() { const { theme, setTheme } = useTheme();
return ( <div className="flex gap-2"> <Button variant={theme === 'light' ? 'primary' : 'ghost'} size="sm" onClick={() => setTheme('light')} > Light </Button> <Button variant={theme === 'dark' ? 'primary' : 'ghost'} size="sm" onClick={() => setTheme('dark')} > Dark </Button> <Button variant={theme === 'system' ? 'primary' : 'ghost'} size="sm" onClick={() => setTheme('system')} > System </Button> </div> );}Tailwind Configuration
Ensure your tailwind.config.js uses the class-based dark mode strategy:
export default { darkMode: 'class', content: [ './src/**/*.{js,ts,jsx,tsx}', './node_modules/@nim-ui/components/dist/**/*.js', ], theme: { extend: {}, },};Best Practices
- Always provide
dark:variants. Every color utility in your custom components should have a correspondingdark:variant to ensure a seamless experience. - Test both modes. Verify that text contrast, borders, and backgrounds look correct in both light and dark modes.
- Use semantic color names. Refer to
neutral-100,primary-500, etc. rather than specific hex codes. This makes theme adaptation automatic. - Prevent flash of incorrect theme. Use the inline
<script>technique to apply the theme before React hydrates. - Offer a system preference option. Respect the user’s OS-level setting as the default, with the option to override.
- Test images and media. Ensure logos, illustrations, and other media look appropriate in both modes. Consider providing separate assets for light and dark themes.
What’s Next?
- Colors - Color palette and design tokens
- Typography - Fonts, sizes, and text styles
- Theming - Advanced theme customization
- Customization - Overriding component styles