Dark Mode
Dark Mode
Verto ships with light and dark themes out of the box. Theme switching is driven entirely by CSS custom properties, so every component, code block, and content element responds to the active theme automatically.
How It Works
Dark mode uses a three-layer approach: CSS variables define the palette, a no-flash script applies the user's preference before first paint, and a toggle component lets visitors switch themes on the fly.
CSS Variables
All colors live as CSS custom properties on :root (light) and html.dark (dark) in app/globals.css. Tailwind v4 picks them up through a @theme block, so you can use standard utility classes like bg-bg, text-text, or border-border anywhere.
The custom variant that wires everything together:
@custom-variant dark (&:is(.dark *));Tailwind maps the variables to design tokens:
@theme {
--color-bg: var(--bg);
--color-text: var(--text);
--color-border: var(--border);
/* ... remaining tokens follow the same pattern */
}When the dark class is present on <html>, every utility referencing these tokens switches to the dark palette with zero extra work.
No-Flash Script
A common problem with class-based dark mode: the page renders in light mode first, then flips to dark after JavaScript loads. Verto avoids this with an inline script in app/layout.tsx that runs before the browser paints anything:
const themeScript = `
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
`;This script is injected via dangerouslySetInnerHTML inside a <script> tag in the root layout, so it executes synchronously before any content renders.
The script also respects the operating system's color scheme preference. If a visitor hasn't explicitly chosen a theme, Verto falls back to prefers-color-scheme: dark from the browser's media query.
ThemeToggle Component
The ThemeToggle component (components/ui/ThemeToggle.tsx) is a client component that handles user interaction. It reads the current theme from localStorage, toggles the dark class on document.documentElement, and persists the choice back to localStorage. A getSystemTheme() helper checks matchMedia('(prefers-color-scheme: dark)') as a fallback when no stored preference exists.
Usage
Content authors don't need to think about dark mode at all. Every MDX component, Shiki code block, and Tailwind-styled element already responds to theme changes through CSS variables. Just write your content and it works in both themes.
CSS Variable Reference
Here are the key variables Verto defines for each theme:
| Variable | Light | Dark |
|---|---|---|
--bg | #ffffff | #0f1117 |
--bg-subtle | #f9fafb | #161b22 |
--bg-muted | #f3f4f6 | #1c2130 |
--border | #e5e7eb | #30363d |
--border-soft | #f0f1f3 | #21262d |
--text | #111827 | #e6edf3 |
--text-muted | #6b7280 | #8b949e |
--text-light | #9ca3af | #484f58 |
--accent | #0f172a | #f0f6fc |
--accent-blue | #2563eb | #58a6ff |
Related
- Syntax Highlighting covers how Shiki renders dual-theme code blocks using these same CSS variables
- Block Components documents the MDX components that automatically adapt to the active theme