OKHST: predictable color generation for real interfaces — tenphi.me<br>Jul 2, 2026 · 17 min read<br>OKHST: predictable color generation for real interfaces<br>csscoloraccessibilitywebdev<br>Wouldn’t it be great if one hue slider could recolor an entire interface,<br>while a saturation slider controlled how colorful the whole system felt?
I don’t mean a beautiful gradient or a decorative palette. I mean the boring<br>parts of an interface: backgrounds, text, borders, hover states, selected<br>states, disabled states, shadows, dark mode, and high contrast.
The premise sounds simple: pick a hue and saturation, then derive everything<br>else. I chased that idea through several color spaces before finding the<br>assumption that kept breaking it:
lightness is not contrast.
OKHST is the coordinate system I eventually built around that distinction. A<br>CSS playground later in the article tests the model in the browser, while Glaze<br>turns it into a production system.
The Old Attempt
About seven years ago, I tried to generate UI colors at runtime. Instead of<br>hand-picking every token, the framework would take a small set of inputs and<br>derive a complete scheme. HSLuv made the experiment<br>possible.
HSLuv gave me something regular HSL did not: a perceptually useful lightness<br>axis. In regular HSL, 50% lightness can mean wildly different things for<br>yellow, blue, red, and green. HSLuv made “make this lighter” and “make this<br>darker” much closer to operations a UI system could rely on.
I spent months tuning formulas around it. Eventually, tiny declarations could<br>color different parts of an interface while the framework kept the palette<br>consistent. The result was good enough for production, and it was one of the<br>most fun things I had built at the time.
The old demo is still online: theme.tenphi.me
But the system was coupled to one design system, embedded in our framework, and<br>tuned for one product. HSLuv was also still immature, so several inconsistencies<br>needed local workarounds.
The experiment convinced me that UI colors could be computed, but it never<br>became a reusable tool. I kept using HSLuv, stopped shipping generated<br>palettes, and waited for a reason to revisit the idea.
The Web Caught Up
That reason arrived with OKLCH in CSS. It gives the web a modern perceptual<br>color space and makes many color operations more sensible than HSL ever could.<br>But direct palette generation remains difficult because C is absolute chroma,<br>not the relative “saturation” control people expect when authoring UI colors.<br>The maximum possible chroma changes with hue and lightness, so keeping chroma<br>fixed while changing lightness can push some colors outside sRGB and cause<br>clipping.
/* Same chroma, different lightness.<br>Some hue/lightness combinations may clip in sRGB. */<br>.step-1 {<br>color: oklch(35% 0.18 250);<br>.step-2 {<br>color: oklch(65% 0.18 250);<br>.step-3 {<br>color: oklch(85% 0.18 250);<br>Manual palettes can work around this: pick each value by eye and adjust the<br>outliers. Dynamic themes cannot do that so easily. When a background, text<br>color, border, shadow, and hover state all derive from one source color,<br>clipping silently changes the relationship between them. A generated ramp can<br>look fine in the middle and fall apart near the edges.
Modern relative colors make the approach especially tempting:
.card {<br>--bg: oklch(65% 0.18 250);
background: var(--bg);<br>color: oklch(from var(--bg) 25% c h);<br>border-color: oklch(from var(--bg) calc(l - 0.12) c h);<br>The relationship is elegant, but the same absolute chroma follows the<br>background into each new lightness value. A generated palette can therefore<br>drift or clip. What I needed was a saturation axis relative to the maximum<br>chroma available at each hue and lightness.
OKHSL Gets Saturation Right
OKHSL provides that missing axis. It has the familiar shape of hue, saturation,<br>and lightness, but builds it on OKLab/OKLCH math. Its saturation is normalized<br>against what is possible for the current hue and lightness.
Near white and black, where highly saturated colors cannot physically exist in<br>sRGB, OKHSL desaturates gracefully instead of clipping unpredictably.
This graceful behavior is what a generated UI palette needs.
// Illustrative pseudo-code, not a real library call.<br>// Same saturation, different lightness — OKHSL keeps it in-gamut.<br>okhsl({ h: 250, s: 70, l: 20 });<br>okhsl({ h: 250, s: 70, l: 50 });<br>okhsl({ h: 250, s: 70, l: 90 });<br>In OKHSL, 70% saturation means something like:
Use 70% of the chroma available for this hue and lightness.
That is much better for generated UI colors than:
Use this absolute chroma value and hope it still fits.
OKHSL is limited to sRGB, which may sound disappointing in a world of wide-gamut<br>displays. For core UI, I often find that constraint useful: repeated text,<br>icons, and borders benefit from a conservative, in-gamut default. Wide-gamut<br>color still has a natural place in illustrations, decorations, and brand moments.
The Bug in My Mental Model
OKHSL solved the gamut problem, but...