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.
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.
: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.
[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
What Building an OTP Input Taught Me About Controlled Components
Auto-advance, paste, and backspace look trivial until you build them — here are the controlled-input lessons that survive every form I ship.
Making a Quran PWA That Works Offline
Building an offline-first Quran app meant a service worker that survives no connection, on-device prayer times for 30+ countries, and a couple of service-worker bugs that taught me humility.