Dark Mode
Mukoko uses next-themes for dark mode management. The theme is applied via a CSS class on the <html> element, and all components adapt automatically through CSS custom properties.
Configuration
1. Install next-themes
pnpm add next-themes2. Create a ThemeProvider
Create components/theme-provider.tsx:
"use client"
import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}3. Wrap your root layout
In app/layout.tsx, wrap {children} with the provider:
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>Key settings:
attribute="class"— adds/removes thedarkclass on<html>defaultTheme="system"— respects the user’s OS preferenceenableSystem— enables automatic system detection
4. Prevent flash of wrong theme
Add suppressHydrationWarning to the <html> element:
<html lang="en" suppressHydrationWarning>This prevents React hydration warnings caused by the theme script that next-themes injects.
CSS custom properties
All theme-adaptive colors are defined twice — once in :root (light) and once in .dark (dark):
:root {
--background: #FAF9F5; /* Warm cream */
--foreground: #141413; /* Near-black */
--card: #FFFFFF;
--muted: #F3F2EE;
--muted-foreground: #5C5B58;
--border: rgba(10, 10, 10, 0.08);
}
.dark {
--background: #0A0A0A; /* Deep night */
--foreground: #F5F5F4; /* Warm white */
--card: #141414;
--muted: #1E1E1E;
--muted-foreground: #9A9A95;
--border: rgba(255, 255, 255, 0.08);
}Tailwind’s dark mode variant is configured with a custom variant in Tailwind CSS 4:
@custom-variant dark (&:is(.dark *));This means dark:bg-card applies when any ancestor has the dark class.
Building a theme toggle
Create a toggle button using the useTheme hook:
"use client"
import { useTheme } from "next-themes"
import { Sun, Moon } from "lucide-react"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
aria-label="Toggle theme"
>
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
)
}Testing dark mode
When developing components, verify both themes:
- Visual check — toggle between light and dark in the browser
- Contrast — ensure text meets APCA 3.0 AAA contrast in both themes
- Borders — check that
--borderprovides enough separation on both backgrounds - Charts — chart colors have separate light/dark values (
--chart-1through--chart-5)
Per-component considerations
- Mineral accent colors are constant across themes —
--color-cobalt,--color-tanzanite, etc. do not change - Semantic tokens (
--background,--foreground,--muted) change between themes - Chart colors have theme-specific values that maintain readability on each background
- Shadows — use
shadow-smandshadow-mdutilities which adapt naturally; avoid hardcoded rgba shadows - Images and logos — use
dark:invertfor simple SVGs, or provide separate assets for each theme