A clean theming and dark mode setup in Next.js 16 + Tailwind v4 with next-themes and OKLCH — and the `@theme inline` gotcha that silently breaks dark mode.
Dark mode in Tailwind v4 is both simpler and trickier than v3. Simpler because config moved to CSS and next-themes does the heavy lifting. Trickier because the new @theme directive has a subtle behavior that silently breaks dark mode if you get it wrong.
Here's a clean theming setup for Next.js 16 + Tailwind v4, and the one gotcha that costs people an afternoon.
You define your design tokens as CSS variables, map them to Tailwind tokens once, and let next-themes flip a class on ``. Components reference semantic tokens (bg-background, text-foreground, bg-primary) and never care which theme is active.
1. Tokens as CSS variables, per theme. In globals.css, define :root { --background: ...; --foreground: ...; --primary: ...; } and override them under .dark { ... }. Use OKLCH values for predictable lightness.
2. Map them with @theme inline. This is the critical line:
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
}The inline keyword tells Tailwind to reference the variable at runtime, so .dark overrides apply. Plain @theme bakes the value at build time and your dark mode silently does nothing — this is the gotcha.
3. next-themes provider. Wrap the app, attribute="class", defaultTheme="system", enableSystem. It toggles .dark on `` and handles the no-flash script.
4. A toggle. A button calling setTheme('dark' | 'light' | 'system'). Done.
1. @theme vs @theme inline. Covered above — it's the number-one dark mode bug in v4. If your colors don't switch, this is almost always why.
2. Flash of wrong theme (FOUC). next-themes injects a blocking script to set the class before paint. Don't render theme-dependent UI before mount if it would mismatch; use suppressHydrationWarning on `` and gate client-only theme reads.
3. OKLCH consistency. Define both themes in OKLCH. Mixing color spaces between light and dark makes transitions and alpha blends look off.
4. Semantic tokens, not raw colors. Reference bg-background, not bg-white dark:bg-zinc-950. Semantic tokens are the entire point — one place to change, both themes follow.
A token-based theme is also what lets an AI assistant restyle your app safely: change a variable, every component follows. Hard-coded dark: variants scattered across components are exactly the kind of inconsistency that compounds when AI (or a teammate) edits later.
Tailwind v4 + next-themes makes dark mode a token problem, not a per-component chore. Define tokens in OKLCH, map them with @theme inline (not plain @theme), and let next-themes flip the class. The whole thing is ~30 lines of CSS plus a provider — once it's right, you never think about it again.
CREA.MBA ships this exact setup: OKLCH tokens for light and dark, @theme inline mapping done right, next-themes wired with no-flash, and a theme toggle in place. Dark mode works out of the box, and because it's all semantic tokens, restyling — by you or your AI assistant — is a one-file change.
Subscribe for more tutorials and tips on building products with AI
The actual integration of Tailwind v4 + shadcn 2.3.0 in Next.js 16 App Router. OKLCH, FOUC-free dark mode, Geist with next/font, and the silent hsl(var()) bug that breaks your styles without warning.
The boilerplate now includes the receiver endpoint to publish posts from a Postiz custom provider. Editorial calendar unified with your social media in under 2 hours.