Skip to main content
All articles
Story July 25, 2026 6 min read

Why We Built Zero-Runtime Theming (No JS Provider)

Every React UI library we tried required a JavaScript theme provider. We thought there was a better way. Here's the story of building CSS-first theming for Ninna UI.

By Ninna UI Team


When we started building Ninna UI, we looked at every theming approach in the React ecosystem: Emotion, styled-components, CSS Modules, CSS-in-JS, theme objects, context providers. They all worked, but they all shared a common cost: JavaScript runtime overhead for something the browser could do natively.

The problem we kept hitting

Every library we tried required a ThemeProvider somewhere in the React tree. This meant: extra JavaScript in the bundle, a context re-render surface when the theme changed, and — on SSR — a hydration step that could flash the wrong theme. The flash was the killer. Users would see a light theme for a frame, then it would switch to dark. It felt broken.

We tried the standard fixes: inline scripts in <head>, blocking hydration, isomorphic theme detection. They worked, but they added complexity. The root cause was always the same: theming was in JavaScript when it should have been in CSS.

The CSS custom properties insight

CSS custom properties (variables) give you dynamic theming without JavaScript. Define colors as variables on :root, override them in .dark, and the browser handles the switch natively. No provider, no context, no re-render, no flash.

The question was: could we build a full component library on this foundation? Could we provide 67 components with semantic colors (primary, success, warning, danger), dark mode, multiple presets, and per-component customization — all through CSS?

The oklch discovery

The missing piece was oklch. With HSL, generating consistent color scales was a manual process — each hue needed individual tuning. With oklch, we could generate perceptually uniform scales programmatically. Pick a hue, step the lightness, and the results were automatically balanced.

This meant our theme presets could be compact CSS files: define 8 semantic colors as oklch values, generate content (text-on-color) pairs, and dark mode overrides. Each preset was under 50 lines of CSS.

What we learned

  • The browser is the best theming runtime — it's instant, flicker-free, and zero-cost
  • CSS custom properties are sufficient for 95% of theming needs — the 5% that needs JS (runtime color picking, dynamic contrast) can be handled with a tiny script
  • oklch makes design systems easier to build, not harder — perceptual uniformity means fewer manual corrections
  • Removing the ThemeProvider simplified everything — no context, no provider tree, no hydration mismatch
  • AI agents understand CSS theming better than JS theming — they don't need to know about context or providers

The result

Ninna UI ships theming as CSS files. You import one preset, and all 67 components are themed. Dark mode is a class toggle. Custom themes are CSS overrides. No JavaScript, no provider, no runtime cost. It's the system we wish existed when we started — and now it does.


Build it with Ninna UI

Accessible React components, CSS-only theming, Tailwind v4 native.

Get Started

Keep reading