Back to Blog
Designing for the System: Implementing Advanced Theme Engines
Photo from Unsplash

Article content

The Flash That Breaks Trust

You've seen it: a user with dark mode enabled loads a page, gets a blinding white flash, then the theme kicks in. That flash is a broken promise — the site asked for their preference and then ignored it for 200ms. On every refresh.

Building a theme engine that's performant, flash-free, and accessible isn't just a polish task. For any app where users return regularly, it's a baseline expectation.

The Modern Stack: next-themes + CSS Variables

The most robust way to handle theming in 2026 is combining the next-themes library with a well-structured CSS Variable system.

1. Defining the Variables

Instead of hardcoding hex values, we define a "semantic" layer in our SCSS:

:root {
  --bg-body: #ffffff;
  --text-main: #000000;
}
 
.dark {
  --bg-body: #0a0a0a;
  --text-main: #ffffff;
}

2. Avoiding the "Flash of Unstyled Content"

One of the biggest challenges with SSR (Server Side Rendering) is the "white flash" when a dark-mode user reloads the page. next-themes solves this by injecting a script into the <head> that applies the correct class before the page even renders.

Best Practices for System Design

  • Semantic Naming: Use names like --accent-color or --border-subtle rather than --blue-500.
  • Optical Adjustments: Colors often need slight shifts in saturation or brightness when moved to dark mode to maintain the same perceived contrast.
  • Motion & Glassmorphism: Ensure transitions are smooth and that glass effects (blur) remain legible across all backgrounds.

Three Tiers of Tokens, Not One

The flat variable list above works for a toggle. It stops working the moment a second brand, a high-contrast mode, or a marketing sub-site arrives. The pattern that scales is a three-tier taxonomy:

  1. Primitive tokens hold raw values and never appear in component code: --grey-900: #0a0a0a, --yellow-400: #facc15.
  2. Semantic tokens map primitives to roles: --bg-body: var(--grey-900), --accent-color: var(--yellow-400). Themes swap at this layer.
  3. Component tokens (optional, for design systems) scope semantics to a component: --button-bg: var(--accent-color).

Components only ever consume semantic or component tokens. When the third theme arrives, you write one new mapping block — no component files change. This site generates its token layer with Style Dictionary from a single JSON source, which means the same tokens can compile to SCSS variables, CSS custom properties, and a TypeScript export for use in JS-driven styling. One source of truth, three output formats, zero drift.

The Details Everyone Misses

color-scheme, the one-line fix for native UI. Your CSS variables don't reach scrollbars, form controls, or the default focus ring. Declare color-scheme: light dark on :root (and the specific value per theme class) and the browser renders native UI to match. Without it, dark mode ships with a glaring white scrollbar on Windows.

Kill transitions during the switch. If your elements have transition: background-color 0.3s, toggling the theme triggers hundreds of staggered crossfades — it reads as lag, not polish. The trick is a temporary class that disables transitions for one frame:

document.documentElement.classList.add('theme-switching');
setTheme(next);
requestAnimationFrame(() =>
  document.documentElement.classList.remove('theme-switching')
);
.theme-switching * { transition: none !important; }

The theme snaps instantly, which is what users actually expect from a toggle.

Three states, not two. A proper toggle cycles light → dark → system. Users who choose "system" get automatic switching at sunset via prefers-color-scheme, and your stored preference should record that they chose system, not the value it resolved to. next-themes models this correctly out of the box — resist simplifying it to a boolean.

suppressHydrationWarning is part of the deal. The head script sets the theme class before React hydrates, so the server-rendered <html> attribute won't match. That one warning suppression on the <html> element is expected and documented, not a hack.

Test the Contrast, Not the Vibe

Dark themes fail accessibility audits more often than light ones, and it's rarely the body text — it's the muted secondary text, the placeholder copy, and the borders that quietly fall below 3:1. Every semantic pair (--text-muted on --bg-body, --accent-color on --accent-foreground) needs checking in both themes, because passing in light says nothing about dark.

Two habits make this sustainable: check contrast at the token level when you define the pair (there's a WCAG AA/AAA checker at mirzaa.dev/tools/contrast-checker built for exactly this), and re-check only the changed tokens in review. Auditing pairs is cheap; auditing rendered pages is endless.

Conclusion

The next-themes + CSS Variables pattern scales cleanly from a simple dark/light toggle to full multi-brand theming. The key discipline is keeping token names semantic — once you start naming tokens after their role (--bg-body, --border-subtle) rather than their value (--grey-100), adding a third theme or a high-contrast accessibility mode becomes a matter of adding one more variable block rather than a find-and-replace across the codebase.

If you're starting fresh: set up the variable structure first, wire next-themes second. Don't add the library before you have a clear token taxonomy — it won't solve the hard part.


Sources & References

  • next-themes on GitHub
  • MDN Web Docs: CSS Custom Properties
  • W3C WCAG: Contrast Requirements
  • Material Design: Dark Theme Guidelines
Newer Post

The AI Multiplier: Beyond Autocomplete in 2026

Older Post

Common React Antipatterns in Enterprise-Scale Apps

Suggested Reading

Architectural Note:This platform serves as a live research laboratory exploring the future of Agentic Web Engineering. While the technical architecture, topic curation, and professional history are directed and verified by Maas Mirzaa, the technical research, drafting, and code execution for this post were augmented by Gemini (Google DeepMind). This synthesis demonstrates a high-velocity workflow where human architectural vision is multiplied by AI-powered execution.