Design
HEX, RGB, and HSL: three ways to say the same color
On this page 0%
I used to think color conversion was a solved library problem. Then I rounded HSL to integers, converted back to hex, and watched a button disagree with itself. The color generator on Randomly has to survive that round trip, because the site’s own green is a hex I actually care about: #009d68. Get the math sloppy and the brand drifts.
This post is hex to RGB, RGB to HSL, where rounding lives, and how a palette picker should talk about zinc neutrals next to that green without inventing extra hues.
Hex is just RGB in a coat
#009d68 means R=0, G=157, B=104, each 0–255. Six hex digits, two per channel. Three-digit shorthand #09d is not this color; that expands by duplicating nibbles. Eight-digit hex adds alpha. I parse by stripping the hash, expanding shorthand, then reading pairs. Garbage in should fail, not become #000000 silently. Silent black is how a typo ships as a void button.
RGB in CSS can be rgb(0 157 104) or the old comma form. Percent forms exist. I normalize to 0–255 integers for display because designers still think in bytes. Internally I keep floats when I convert to HSL so I do not quantize twice.
Relative luminance for contrast is a different formula (sRGB to linear, then the WCAG weights). Do not use the HSL lightness as a contrast check. I have failed an accessibility pass because L=45% looked “middle” and the actual contrast against zinc-900 was mush.
RGB to HSL
Divide R, G, B by 255 to get 0–1. Max, min, delta. Lightness L = (max + min) / 2. Saturation is 0 if delta is 0, else delta / (1 − |2L − 1|). Hue is the annoying piece: 60° times a combination of which channel won, then wrap to 0–360.
Grays: delta 0, hue is undefined in theory. In practice I store hue as 0 so the UI has a number. Changing hue on a gray should not explode. Changing saturation on a gray should grow a color along that hue. If hue is NaN, the slider is haunted.
HSL is not perceptually uniform. Two colors with the same L do not look equally light. HSL is still the least-bad cylinder for a simple picker. If I needed perceptual spacing I would be in OKLCH and this post would be longer. The generator stays on hex, RGB, HSL because that is what people paste into CSS.
Rounding is the bug
CSS often wants integer RGB and integer HSL percents. Integer HSL is a lossy codec. Convert hex to HSL, round H/S/L, convert back, and you may not get the same hex. #009d68 should survive a careful round trip if you keep floats internally and only round at the display edge. If you round at every step, you walk.
I keep a canonical hex as source of truth when the user picked a hex. Derived HSL is display. If the user drags HSL sliders, HSL becomes source and hex is derived. Last input wins. Dual sources fight.
Percent saturation of 0.4% rounding to 0% is how a near-gray becomes a true gray and the hue slider looks dead. Show one decimal in the UI if you need to, or keep full precision in memory. I prefer memory plus integer display, with a warning that copied HSL integers might not invert.
The site green and zinc
Randomly’s primary is that green, #009d68. Neutrals are zinc-like grays: cool, not warm beige. A palette generator should not “harmonize” by rotating hue 30° and calling it a brand system. I want tints and shades of the green, plus a zinc ramp that stays on the gray axis. If a tool injects extra chroma into the neutrals, screenshots look dirty.
Practical recipe: take #009d68, convert to HSL, vary L for a green scale, keep S from collapsing to 0 until you actually want gray. Build zinc separately as low-S, hue around 240 or whatever your zinc uses, stepped L. Do not average green and zinc in RGB to make a “bridge” color. You will get a swamp.
On dark UI, the green needs a lighter sibling for text on dark-green chips. Contrast against zinc-950 and zinc-50 separately. The generator can emit both. Your eyes still have to check. Formulas do not replace a screenshot.
Palettes without the mysticism
Complementary hue is +180. Analogous is ±30. Split complementary is a design-school poster. Useful for a “give me five colors” button. Dangerous as a brand process. I still ship those modes because people ask. The default I want for this site is: one brand hue, one neutral ramp, one danger red that is not the complementary of green unless it accidentally works.
Random palettes should use a CSPRNG or at least not Math.random() if you also use the same helper for tokens. Colors are not secrets. I still like one random helper. See fair dice for the sampling lecture; here it only matters so two “random palette” clicks are not a visible LCG pattern of yellows.
CSS and copy
Copy hex with the leading hash. Copy RGB as modern space-separated syntax unless the user is in a hex-only field. Copy HSL as hsl(152 100% 31%) style if that matches the computed values; do not pretend those percents are sacred if they came from a rounded trip.
Named CSS colors are a trap. green is not #009d68. seagreen is not either. If a generator maps “closest named color,” label it as closest, not as identity.
What I check before shipping a theme
Primary hex matches the brand token. Hover state is a darker or lighter sibling from the same hue. Focus rings contrast on both light and dark shells. Charts do not rely on green versus gray for the only distinction (add a pattern or a label). If I need another accent, I pick it on purpose, not from a random complementary spin.
The generator is a scratchpad. The design tokens in the repo are the source. Do not reverse that.
Alpha and other spaces
Eight-digit hex #009d68ff is the same green with opaque alpha. Dropping alpha on copy when the user did not ask for transparency is correct for most CSS variables we use. Keeping a stray ff in a design token file is how two “same” colors fail a string compare in tests. I strip alpha when it is fully opaque. I keep it when it is not. HSL with alpha is hsl(H S% L% / A). Do not invent a fourth HSL channel named “hex.”
Display-P3 and OKLCH are showing up in modern CSS. The picker still speaks sRGB hex because that is what our current tokens are. If I convert to P3 and back without a profile, I am lying. Better to say “sRGB hex” on the page than to pretend a wider gamut. When we move tokens, we will move the tool. Not before.
For charts next to the green, pick a second hue with enough distance that deuteranopia still splits the series. A random complementary might land on a red that collides. Check a simulation. The generator will not do that for you. It will give you numbers. You still look.
Frequently asked questions
Why does HSL not match the hex after I copy both?
Integer rounding. Keep one canonical representation. Treat the other as a preview.
Is #009d68 152° hue?
Approximately, depending on rounding. Compute it, do not memorize a blog’s rounded HSL as law.
Should I design in HSL or hex?
Store hex (or OKLCH if your stack is there). Edit however you can see. Ship hex in the CSS variables we already use.
Does the color tool upload my palette?
No. It runs in the browser. Privacy for the rest.