Two Color Schemes, Four Modes: Native CSS Theme Switching
These articles are AI-generated summaries. Please check the original sources for full details.
Two Color Schemes, Four Modes: Native CSS Theme Switching.
Olga Urentseva demonstrates a native CSS approach to managing four distinct theme variants through the light-dark() function. The system enables a default and a secondary color scheme, each supporting light and dark modes with zero JavaScript for color values.
Why This Matters
Modern frontend development is shifting toward vanillaization to leverage native browser features, reducing reliance on heavy JavaScript state management. While @container style() queries represent an ideal model for theme switching, current lack of support in Safari and Firefox makes specificity-based CSS overrides the more technical reality for cross-browser production environments.
Key Insights
- The :root:root:root specificity hack prevents global CSS variables from being overridden by injected styles from tools like styled-components.
- Native light-dark() functions allow browsers to automatically select values based on system preferences without JavaScript intervention.
- Applying theme classes synchronously in the main entry point (main.tsx) prevents the flash of unstyled content (FOUC) on page reload.
- CSS specificity hierarchy allows .spring:root:root:root to reliably override default :root variables when a class is applied to the HTML tag.
- Browser compatibility limitations currently prevent the use of @container style() queries in Firefox Developer Edition and Safari as of 2026.
Working Examples
Specificity-based theme overrides using native light-dark() functions.
:root:root:root {
--color-background: light-dark(oklch(0.99 0.0105 320.98), oklch(13.709% 0.02553 268.319));
--color-primary: light-dark(oklch(70.61% 0.085 271.69), oklch(0.79 0.1233 266.14));
}
.spring:root:root:root {
--color-background: light-dark(oklch(0.99 0.012 150), oklch(0.18 0.05 145));
--color-primary: light-dark(oklch(56.316% 0.10067 150.907), oklch(0.72 0.2 145));
}
Zero-state UI toggle logic using native DOM classList API.
function handleToggle() {
const isSpring = document.documentElement.classList.toggle("spring");
localStorage.setItem("theme", isSpring ? "spring" : "default");
}
Practical Applications
- Vite-based production builds: Use a single CSS file with class-based overrides to avoid the unreliability of asynchronous dynamic CSS imports.
- Pitfall: Implementing themes via React state or Context can lead to unnecessary re-renders and layout shifts compared to direct DOM class manipulation.
- Legacy Integration: Use the triple :root selector to ensure global variables maintain priority in projects still utilizing styled-components or other CSS-in-JS libraries.
- Pitfall: Relying on light-dark() without @media (prefers-color-scheme) fallbacks will result in broken themes on browsers lacking modern CSS support.
References:
Continue reading
Next article
Solving Gitaly Memory Spikes: Why Cgroup v2 is Critical for GitLab on Kubernetes
Related Content
Mastering Multi-State UI with the CSS Radio State Machine
Amit Sheen details the Radio State Machine, a CSS-only technique for managing complex, multi-state visual UI without JavaScript overhead.
Implementing Zigzag CSS Grid Layouts Using the Transform Trick
Learn how to build staggered zigzag layouts using CSS Grid and translateY(50%) while maintaining accessible DOM order and responsive flow.
Building Scrollytelling Experiences with CSS Scroll-Snap Events and Scroll-Driven Animation
Lee Meyer demonstrates how to utilize emergent Chromium-based scroll-snap events and scroll-state queries to create complex, interactive scrollytelling experiences.