Dark mode is no longer a "nice to have" — it is a baseline expectation. Users who set their OS to dark mode expect every website to respect it. Getting dark mode right means: correct colors, no flash on page load, and a working manual toggle that persists across sessions. Here is the complete 2026 approach.
What changed in 2026
light-dark() CSS function is now supported in all evergreen browsers. It lets you declare both values in one property: color: light-dark(#111, #eee); — no media query needed for individual properties.
next-themes 1.x is the standard library for Next.js dark mode; it handles FOUC prevention and system preference detection with minimal code.
- Tailwind CSS 4 defaults to CSS variables internally, making custom property theming and Tailwind
dark: utilities work together without conflicts.
- CSS
color-scheme property tells the browser to use the OS dark scrollbars, form controls, and focus rings — set it as well as your custom colors.
Approach 1 — CSS custom properties (framework-agnostic)
Define tokens in :root and override in a [data-theme="dark"] block:
/* globals.css */
:root {
--color-bg: #ffffff;
--color-surface: #f5f5f5;
--color-text-primary: #111111;
--color-text-secondary: #555555;
--color-border: #e0e0e0;
--color-accent: #2563eb;
color-scheme: light;
}
[data-theme="dark"] {
--color-bg: #0f0f0f;
--color-surface: #1a1a1a;
--color-text-primary: #f0f0f0;
--color-text-secondary: #aaaaaa;
--color-border: #2a2a2a;
--color-accent: #60a5fa;
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg: #0f0f0f;
/* ... same overrides */
color-scheme: dark;
}
}
Set data-theme on <html> via JavaScript and persist in localStorage.
Approach 2 — light-dark() CSS function
For individual properties without a full token system:
:root { color-scheme: light dark; }
body {
background: light-dark(#ffffff, #0f0f0f);
color: light-dark(#111111, #f0f0f0);
}
Responds to prefers-color-scheme automatically. Combine with a data attribute for manual override:
[data-theme="light"] { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
Tailwind CSS dark mode
In tailwind.config.js:
export default {
darkMode: 'class',
// ...
};
Toggle the dark class on <html>:
document.documentElement.classList.toggle('dark', isDark);
Use dark: utilities in markup:
<div className="bg-white dark:bg-zinc-900 text-black dark:text-white">
Content
</div>
React + Next.js with next-themes
npm install next-themes
In app/layout.tsx:
import { ThemeProvider } from 'next-themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="data-theme" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
);
}
suppressHydrationWarning is required because next-themes modifies the <html> attribute on the client before React hydrates, which would otherwise trigger a hydration mismatch warning.
Dark mode toggle component:
'use client';
import { useTheme } from 'next-themes';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? 'Light' : 'Dark'}
</button>
);
}
Preventing the flash of unstyled content (FOUC)
The flash happens when the page renders with the default (light) theme before JavaScript reads localStorage and applies the user preference. Fix it with a blocking inline script:
<!-- In <head>, before any CSS -->
<script>
(function() {
var theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
next-themes handles this automatically when used in Next.js App Router.
Color system comparison
| Approach |
Complexity |
System preference |
Manual override |
Tailwind compat |
prefers-color-scheme only |
Low |
Yes |
No |
Partial |
| CSS custom properties + data attribute |
Medium |
Yes |
Yes |
Yes |
light-dark() function |
Low |
Yes |
With color-scheme |
No |
next-themes (React) |
Low |
Yes |
Yes |
Yes |
How to pick
- Plain HTML/CSS site? → CSS custom properties +
prefers-color-scheme + data-theme for manual toggle.
- Next.js app? →
next-themes + Tailwind class strategy; covers FOUC automatically.
- Need per-component granularity? → CSS custom properties; each component reads from the token.
- Minimal approach, no manual toggle? →
light-dark() CSS function.
Common mistakes
Not persisting the user choice. If localStorage is not used, the theme resets on page reload.
Hardcoded color values alongside tokens. color: #111 in a component overrides the token and stays light in dark mode. Use only token-based colors.
Flash on first load. Missing the blocking inline script causes the default theme to show briefly. Test in incognito with a dark localStorage value to verify no flash.
Not setting color-scheme. Without it, browser UI elements (scrollbars, form inputs, date pickers) stay light even when your custom colors are dark.
What to skip
- Duplicating all CSS with
.dark prefix. CSS custom properties exist precisely to avoid this.
- A custom dark mode hook from scratch in Next.js.
next-themes handles edge cases (SSR, hydration) that custom hooks miss.
- System theme only with no manual override. Users on light system themes sometimes prefer dark on specific sites. Always provide a toggle.
FAQ
Should images be different in dark mode?
Photographs usually work fine. Logos with transparent backgrounds often need a dark variant. Use the <picture> element with a prefers-color-scheme media query for images that need to change.
How do I test dark mode in Chrome?
DevTools → Rendering tab → "Emulate CSS media feature prefers-color-scheme" → dark.
Does dark mode affect SEO?
No. Dark mode is purely visual and runs client-side. Search crawlers see the same HTML regardless.
What colors work well in dark mode?
Avoid pure black (#000) backgrounds — they create too much contrast. Use a very dark gray (e.g. #0f0f0f or #111827). Accent colors often need to be lightened (increase lightness by 15–20 %) to maintain sufficient contrast against dark backgrounds.
Where to go next