# Dark Mode Toggle
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.
## What's included
- `ThemeProvider` — context provider; reads localStorage on mount, listens for OS preference changes when mode is `system`
- `useTheme()` — returns `{ theme, resolved, setTheme, cycleTheme }` — `theme` is the stored preference, `resolved` is always `'light'` or `'dark'`
- `ThemeToggle` — button that cycles light → dark → system; renders a fixed-size placeholder before mount to prevent layout shift
- `THEME_SCRIPT` — inline script string to paste into `<head>` before stylesheets; eliminates flash of wrong theme on first load
- `useColorScheme()` — standalone hook returning the OS preference; usable without `ThemeProvider`
- `Theme` type — `'light' | 'dark' | 'system'`
- `ResolvedTheme` type — `'light' | 'dark'`
## Setup
### 1. Install dependencies
No extra packages — only React and Tailwind, which you already have.
### 2. Inject the no-flash script
```tsx
// app/layout.tsx
import { THEME_SCRIPT } from '@/blocks/darkmode'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />
{/* stylesheets go after */}
</head>
<body>{children}</body>
</html>
)
}
```
### 3. Wrap your app
```tsx
// app/providers.tsx
'use client'
import { ThemeProvider } from '@/blocks/darkmode'
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider defaultTheme="system">{children}</ThemeProvider>
}
```
```tsx
// app/layout.tsx — inside <body>
import { Providers } from './providers'
<body><Providers>{children}</Providers></body>
```
### 4. Tailwind config
```js
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
```
### 5. CSS variables (globals.css)
```css
:root { --bg: #fff; --text: #111; --border: #d1d5db; }
.dark { --bg: #100F0A; --text: #F4F3EF; --border: #374151; }
```
## Usage examples
```tsx
// Drop-in toggle button — put it in your navbar
import { ThemeToggle } from '@/blocks/darkmode'
<ThemeToggle /> // icon only
<ThemeToggle showLabel /> // icon + "Light" / "Dark" / "System" label
```
```tsx
// Programmatic control
'use client'
import { useTheme } from '@/blocks/darkmode'
export function ThemeSelect() {
const { theme, setTheme } = useTheme()
return (
<select value={theme} onChange={e => setTheme(e.target.value as any)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
)
}
```
```tsx
// Read resolved theme to swap assets
'use client'
import { useTheme } from '@/blocks/darkmode'
export function Logo() {
const { resolved } = useTheme()
return <img src={resolved === 'dark' ? '/logo-white.svg' : '/logo-black.svg'} />
}
```
## Notes
- `suppressHydrationWarning` on `<html>` is required — the no-flash script mutates the DOM before React hydrates, which would otherwise trigger a hydration mismatch warning
- `THEME_SCRIPT` must be the first `<script>` in `<head>`, before any stylesheet `<link>` tags — if it runs after CSS you'll still get a flash
- `ThemeToggle` renders a dimensioned empty `div` before mount instead of `null` — this prevents layout shift in navbars; pass a custom `style` prop to adjust the placeholder size
- Storage key defaults to `ms-theme` — if you run multiple apps on the same domain (e.g. localhost dev), pass a custom `storageKey` to `ThemeProvider` to avoid conflicts between them