Zero-Runtime CSS Theming: Why CSS Variables Beat CSS-in-JS
JavaScript theme providers ship extra JS, cause re-renders, and create hydration mismatches. CSS custom properties do the same job with zero runtime cost.
By Ninna UI Team
For years, CSS-in-JS was the standard for theming React apps. Libraries like Emotion and styled-components gave us dynamic styling, scoped classes, and theme objects. But the cost was real: extra JavaScript in the bundle, runtime style computation, and hydration mismatches on SSR.
The cost of CSS-in-JS theming
- The theme object and runtime ship in your JS bundle (typically 5-15KB gzipped).
- Theme changes can trigger React re-renders across the component tree.
- On SSR, there's a window where server-rendered styles and client styles disagree — the flash of unstyled or wrong-styled content.
- Style computation happens at runtime — every render potentially generates new CSS.
CSS custom properties: the zero-runtime alternative
CSS custom properties (variables) give you dynamic theming without any JavaScript. The browser is the runtime — switching themes is just changing which variables apply:
:root {
--color-primary: oklch(0.55 0.22 280);
--color-primary-content: oklch(0.98 0.01 280);
}
[data-theme="dark"] {
--color-primary: oklch(0.72 0.18 280);
--color-primary-content: oklch(0.15 0.02 280);
}No provider, no context, no re-render. The browser handles the switch natively. This is exactly how Ninna UI ships theming.
What you lose (and it's less than you think)
CSS-in-JS gave us: dynamic styles based on props (use className with conditional logic instead), TypeScript autocomplete on theme keys (use Tailwind's built-in types), and co-located styles (use data-slot CSS targets). None of these require a JavaScript runtime.
The Tailwind v4 factor
Tailwind CSS v4 made CSS-first theming practical. The @theme directive maps CSS variables to utility classes, so you get bg-primary, text-success, border-warning — all driven by CSS custom properties. No config file, no JavaScript.
@import "tailwindcss";
@import "@ninna-ui/core/theme/presets/default.css";
/* That's it. 8 semantic colors, dark mode, all via CSS. */The verdict
For 2026 projects, zero-runtime CSS theming is the better default. You get smaller bundles, faster hydration, no theme flash, and simpler architecture. CSS-in-JS still has niche use cases, but for most apps, CSS variables + Tailwind v4 is the way.