All writing
6 min read

Theming Without a Build Step: How NYX Uses color-mix()

I built NYX, a zero-dependency component framework, so a whole theme can be retuned from a handful of custom properties — no Sass, no recompile, live in the browser.

CSSDesign SystemsOpen Source

When I built NYX — my zero-dependency CSS + JS component framework — I had one hard rule: changing the theme must never need a build step. No Sass recompile, no token pipeline to rerun. Drop in the stylesheet, set a few custom properties, and the whole system retunes itself live. The thing that makes that possible is color-mix().

The problem with one variable per shade

The naive way to theme with custom properties is to define every shade by hand: a base, a hover, a muted, a border, a subtle fill. That is five variables for one colour — and the moment someone wants a new brand colour they have to redefine all five and keep them in harmony. It does not scale, and it is exactly the kind of busywork a framework should erase.

One source colour, derived everywhere

With color-mix() you declare the brand colour once and derive the rest in CSS, at render time. Need a hover that is a little lighter? Mix the brand toward white. Need a subtle background tint? Mix it into the surface at low strength.

css
:root {
  --nyx-primary: #386dff;
}
.nyx-btn {
  background: var(--nyx-primary);
}
.nyx-btn:hover {
  /* lift toward white, no second variable needed */
  background: color-mix(in oklch, var(--nyx-primary) 85%, white);
}
.nyx-alert {
  /* a soft tint of the same hue */
  background: color-mix(in oklch, var(--nyx-primary) 12%, transparent);
}

Change --nyx-primary and every derived colour follows. One variable, a whole coherent palette.

Why oklch matters here

I mix in the oklch colour space on purpose. Mixing in sRGB tends to muddy through grey and shift the hue in ways that look wrong. OKLCH is perceptually uniform, so a mix between two colours stays in the right lightness and hue lane — the lifted hover actually reads as the same colour, brighter, instead of a slightly different one.

Light and dark from the same tokens

Because everything derives from a small set of properties, switching themes is just swapping those properties under a data-theme attribute — not maintaining a parallel stylesheet.

css
[data-theme="dark"] {
  --nyx-bg: #070707;
  --nyx-ink: #ffffff;
}
[data-theme="light"] {
  --nyx-bg: #ffffff;
  --nyx-ink: #0a0a0a;
}

The components never reference a literal colour — only the tokens — so they follow the theme automatically, including every color-mix() derivation.

The payoff

NYX ships 100+ components and a tiny UMD runtime, and you can reskin the entire thing from the browser console by setting a few variables. No build, no dependency, first-class RTL because it is all logical properties underneath. That is the whole philosophy: the platform got good enough that the framework can get out of the way. You can poke at it on GitHub at github.com/fadyehabamer/NYX.


Written by Fady Ehab Amer

Get in touch →

Keep reading