Why lightening a hex color in RGB gives you grey (and the HSL fix)
Your brand color has one hex value. A design system needs about nine: a light tint for hover backgrounds, a dark shade for pressed states, something in between for borders. The obvious way to generate them is to nudge the RGB channels up and down. It looks fine until you put the swatches side by side and notice the light end has gone grey and the dark end has gone muddy. Here's why that happens, and how to build a scale that actually stays on-hue. Take a saturated blue and lighten it the naive way — push every channel toward 255: const lighten = (r, g, b, amount) => [ Math.round(r + (255 - r) * amount), Math.round(g + (255 - g) * amount), Math.round(b + (255 - b) * amount), ]; lighten(37, 99, 235, 0.6); // #2563eb at 60% → [168, 192, 243] That result, #a8c0f3, isn't a lighter version of the same blue. As all three channels race toward 255 they converge, and the gap between them — which is what carries the hue — shrinks. The color desaturates on its way up. Do the same trick downward toward 0 and the channels collapse together again, so your dark shades drift toward a dead near-black instead of a rich navy. RGB is a hardware model. It describes how much light each phosphor emits, not anything a human would call "the same color but lighter." Lightness and hue are tangled together in it, so you can't move one without disturbing the other. HSL splits a color into Hue, Saturation, and Lightness. If you convert your brand color once, you can slide L up and down while H and S stay put — which is exactly the "same color, different brightness" operation you actually wanted. function hexToHsl(hex) { let [r, g, b] = hex.match(/\w\w/g).map((x) => parseInt(x, 16) / 255); const max = Math.max(r, g, b), min = Math.min(r, g, b); const l = (max + min) / 2; let h = 0, s = 0; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); if (max === r) h = ((g - b) / d + (g `hsl(${h.toFixed(0)} ${s.toFixed(0)}% ${l}%)`); } scale("#2563eb"); // hsl(217 83%