UI · Starter
Dark Mode
Flash-free dark mode for Next.js 14+ with Tailwind — three-way light/dark/system toggle, localStorage persistence, OS preference sync, and hydration-safe rendering via next-themes.
Read the Getting Access guideif you haven't yet.
1. Get the file
Sign in at marrowstack.dev, open the Dark Mode block, and click Copy all files. Paste lib/darkmode.ts and the ThemeToggle component into your project.
2. Prerequisites
- Next.js 14 or 15, App Router
- Tailwind CSS with
darkMode: 'class'set in your config - TypeScript 5+
3. Install
- Copy the block files into your project.
- Install peer dependencies:
npm install next-themes4. Configure Tailwind
Enable class-based dark mode in your Tailwind config — this is required for dark: variants to work:
import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: 'class', // ← required
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
// ...
}
export default config5. Wire it in
Wrap your root layout with ThemeProvider. The suppressHydrationWarning on <html> prevents the React hydration mismatch that occurs when the theme is resolved client-side:
import { ThemeProvider } from '@/lib/darkmode'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}'use client'
import { ThemeToggle } from '@/lib/darkmode'
// Drop anywhere in your nav — cycles through light → dark → system
export function Navbar() {
return (
<nav>
<ThemeToggle />
</nav>
)
}'use client'
import { useTheme } from 'next-themes'
export function CustomToggle() {
const { theme, setTheme, resolvedTheme } = useTheme()
return (
<button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>
{resolvedTheme === 'dark' ? 'Switch to light' : 'Switch to dark'}
</button>
)
}Use Tailwind's dark: prefix to style any element conditionally:
<div className="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<p className="text-sm dark:text-gray-400">Adapts to the active theme.</p>
</div>6. Verify it works
- Click the
ThemeToggleand confirm the page switches between light and dark without a flash. - Refresh the page and confirm the theme persists (stored in
localStorage). - Set theme to
systemand toggle your OS dark mode preference — confirm the page follows. - Open DevTools → Elements and confirm the
darkclass is added to and removed from<html>.
7. Failure modes & fixes
Flash of wrong theme on page load
Cause: suppressHydrationWarning is missing from the <html> tag, or ThemeProvider is not the outermost wrapper.
Fix: Add suppressHydrationWarning to <html lang="en" suppressHydrationWarning>. Ensure ThemeProvider wraps all children in the root layout.
dark: Tailwind classes have no effect
Cause: darkMode is set to 'media' or not set in tailwind.config.ts.
Fix: Set darkMode: 'class' in tailwind.config.ts. The block uses the class strategy — 'media' ignores the ThemeProvider.
Theme resets to default on every page navigation
Cause: ThemeProvider is mounted inside a Client Component that remounts on navigation.
Fix: Move ThemeProvider to the root app/layout.tsx (Server Component), not inside any client layout or page component.