Skip to Content
DocsDark Mode

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-themes

2. 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 the dark class on <html>
  • defaultTheme="system" — respects the user’s OS preference
  • enableSystem — 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:

  1. Visual check — toggle between light and dark in the browser
  2. Contrast — ensure text meets APCA 3.0 AAA contrast in both themes
  3. Borders — check that --border provides enough separation on both backgrounds
  4. Charts — chart colors have separate light/dark values (--chart-1 through --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-sm and shadow-md utilities which adapt naturally; avoid hardcoded rgba shadows
  • Images and logos — use dark:invert for simple SVGs, or provide separate assets for each theme
Last updated on