Every Flexpert project ships with light, dark and system themes. It sounds like a checkbox feature — until you're three months in, the palette has doubled, and a single color change means hunting through forty components. The difference between a theme system that scales and one that collapses comes down to architecture decided on day one.
Start with tokens, not colors
The most common mistake is theming at the component level — sprinkling dark: variants everywhere. It works for a demo and rots in production. Instead, define a single layer of semantic tokens and let every component consume those.
A token like --surface or --text-muted describes a role, not a value. The theme decides what each role resolves to. Components never know which theme is active — they just ask for the role.
/* light = default */ :root { --bg: #FFF8F8; --surface: #FFFFFF; --text: #0F172A; } [data-theme="dark"] { --bg: #310038; --surface: #3E0A45; --text: #FFFFFF; }
Name by role, never by value
If you find yourself writing --gray-800 and using it for text, you've already coupled the value to the usage. The moment dark mode needs that text to be near-white, the name lies. Name the role: --text, --border, --brand.
Respect the system preference
System mode should be the default on first visit. Read prefers-color-scheme, but always let the user override it — and persist that choice. The order that works:
- On load, read the saved preference from storage.
- If it's “system” (or unset), resolve against the OS media query.
- Apply the resolved theme before first paint to avoid a flash.
Make switching feel intentional
A hard flash between themes feels broken. A brief crossfade feels designed. Keep it short — around 300ms — and avoid transitioning every property on the page, which tanks performance. Transition the handful that matter.
The goal isn't a flashy animation. It's the absence of a jarring one.
A few rules we follow:
- Never transition
coloron huge text-heavy pages — batch it onbodyinstead. - Use a veil overlay for the crossfade rather than animating thousands of nodes.
- Respect
prefers-reduced-motionand skip the animation entirely.
Key takeaways
A theme system scales when the surface area of change stays constant as the product grows. Add a component, and it inherits theming for free. Change a brand color, and you touch exactly one line. Get the token layer right and everything above it gets simpler — which, not coincidentally, is the same principle behind every good design system.