Colour Studio
Gradients, palettes and contrast in one place. Build a gradient and copy the CSS, generate a palette that actually holds together, and check whether your text clears WCAG before a reviewer does it for you.
Preview
Live — updates as you typeCSS
Paste straight into a stylesheet. Stops are emitted in position order, so the output is always valid CSS even if you reorder them here.
Stops
Click a swatch to open your system colour picker, or type a hex directly. Up to six stops — more than that and a gradient starts to band on cheap panels.
Direction
0° runs bottom-to-top, 90° left-to-right, 180° top-to-bottom. For a hero section, something diagonal between 120° and 160° reads as deliberate rather than accidental.
Base colour
Everything else is derived from this one value by rotating hue and shifting lightness, so the set always reads as a family rather than five unrelated swatches.
Harmony
Analogous is the safe choice for interfaces — neighbours on the wheel never clash. Complement and Triadic give you a genuine accent colour, but use it sparingly: one accent, everything else neutral.
Palette
Click any swatch to copy its hex. The label underneath is the nearest colour name — handy when you need to describe a shade in a design review.
How to use it
- 60 / 30 / 10. Most of the surface, a secondary tone, and one accent. Not five equal stripes.
- Check contrast, don't eyeball it. Switch to the Contrast tab with your text and background before you commit.
- Dark mode isn't inverted light mode. Desaturate and lift lightness rather than flipping the values.
Preview
Ratio
Large text means 18.66px bold or 24px regular. Body copy gets no such discount — aim for AA at minimum, and AAA if the text is small, thin or on a busy background.
Colours
Transparency is ignored — the ratio is computed from the two opaque values, which is what you should be testing anyway. If you're layering glass effects, flatten them first and check the result.
Passing AA doesn't mean it's comfortable. 4.5:1 is a floor, not a target. Long paragraphs read noticeably better around 7:1, and anything below 4.5:1 will be genuinely unreadable for a good number of people.
Contrast is a formula, not an opinion
A contrast ratio comes from relative luminance, which is computed rather than judged. Three things go into it, and each one explains a rule that otherwise looks arbitrary.
The first is a gamma decode. The numbers in a hex code are not proportional to light: they are encoded so that the available steps are spread the way the eye resolves them, which means the arithmetic needs to undo that before it can measure anything. Each channel is divided by 12.92 when it is small, or raised to the power of 2.4 after a small linear shift when it is not.
The second is the weighting. Once the channels are linear, green carries 71.5% of the luminance, red 21.3% and blue 7.2%. That is why a saturated green reads as bright and a saturated blue reads as dark at the same nominal lightness, and why "make the accent darker" means a different change for every hue.
The third is the ratio itself: the lighter luminance plus a small constant, divided by the darker one plus the same constant. That constant is what keeps the formula finite when the darker colour is black, and it is why the scale tops out:
white on black
L1 = 1.000 L2 = 0.000
(1.000 + 0.05) / (0.000 + 0.05) = 21.0
21:1 is the maximum any pair of colours can reach.
4.5:1 — the AA floor for body text — is not halfway down
that scale in appearance, only in the ratio.
This is the part worth carrying into a review. A disagreement about whether two colours are legible enough has an answer, and it is not a matter of taste — which means it can be settled in a minute instead of a week. What the number cannot tell you is whether the result is comfortable. The floor is a floor: a long paragraph at exactly 4.5:1 is legible and tiring, and the same paragraph at 8:1 is noticeably easier to read. Aim above the minimum for anything longer than a heading.
The checker ignores alpha, and that matters more than it sounds. A hex code can carry an eighth digit for opacity, and a colour at half opacity does not have the contrast its six digits claim — it has the contrast of the result after it is composited onto whatever is behind it. A white label at 60% opacity over a pale panel is not white on grey, it is a slightly lighter grey on grey. Flatten the pair first, then measure.
Hue rotation holds a family together
Two colours built from the same saturation and lightness at different hues read as relatives even when they are far apart on the wheel, because everything about them but the hue is identical. That is the whole trick behind a scheme that holds together: keep two of the three values fixed and move the third.
What breaks it is assuming the hue wheel is evenly spaced in appearance. It is not. Sixty degrees is a small step near yellow and a large one near blue-violet, because the eye resolves more distinct hues in some parts of the spectrum than others, and because the model is not perceptually uniform. A scheme built by rotating every colour by the same number of degrees looks reasonable on paper and uneven on screen — one pair plainly related, another apparently unrelated. Rotating by smaller amounts and checking by eye is faster than trusting the arithmetic.
A ramp is a decision about lightness
Building a set of shades by moving HSL lightness alone produces a ramp that fades toward grey at the pale end while every value in it still reports full saturation. Nothing in the numbers is wrong; the eye stopped agreeing with the model. Perceptual lightness is a separate quantity, and a model built on it — one where equal steps look like equal steps — is what makes a ramp read evenly from end to end.
The same problem shows up in dark mode, in a form that is worse because it affects a whole interface. Flipping the lightness of every colour and keeping the hues produces a dark theme that looks like the light one put through a filter: the accents are too saturated, the greys too blue, and the contrast too harsh. Dark mode is not an inversion. It is a second palette built for the same relationships at different light levels, which is why the ramps in one rarely transfer unchanged to the other.
Ship the hex, keep the relationship
A hex code is a storage format and a good one — exact, universal, accepted by every tool in the chain. What it cannot do is carry a relationship. Nothing in those six digits says which other colours it belongs with, what a step lighter would be, or why it was chosen. A palette of five hex values pasted into a stylesheet works perfectly and documents nothing.
That is the argument for naming them. A token called --accent-hover says what the value is for; the same value inlined at a call site says nothing and cannot be changed in one place. Where the values came from a perceptually even ramp, keeping the recipe alongside them is worth more than the values themselves, because the next colour — the one nobody has needed yet — is then a step rather than a guess.
Reference
What each colour model is good at
| Model | This colour | What it is good at | Where it misleads you |
|---|---|---|---|
| HEX | #FF6B35 | Talking to machines. A stylesheet, a design file, a screenshot annotation and an email all carry the same value, and nothing has to be converted on the way. | It keeps the answer and throws away the working. Nothing in those digits says this is a saturated orange, that its hue sits at 16°, or what one step lighter of it would look like. |
| RGB | rgb(255, 107, 53) | Anything that needs the channels apart — alpha compositing, canvas and WebGL, or reading values off a screenshot with a picker. | The values are gamma-encoded, so they are not proportional to light: rgb(128, 128, 128) gives off about 22% of the light that white does, not half. Brightness arithmetic on these numbers goes wrong quietly. |
| HSL | hsl(16, 100%, 60.4%) | Thinking out loud in the words designers use, and editing by hand. Turn the hue for an analogous palette, pull the saturation for a muted version, and the value stays readable. | The saturation is not how colourful it looks and the lightness is not how bright it looks. Hold saturation at 100%, raise lightness to 80%, and you get a peach rather than the same orange turned up. |
| OKLCH | oklch(0.7045 0.1926 39.23) | Building sets. Equal steps in lightness look like equal steps to the eye, so a ramp reads evenly from end to end and dark mode stops looking like light mode with the values flipped. | Almost nobody reads it fluently, and plenty of design tools still want a hex in the colour field and nothing else — so you end up shipping the hex anyway. |
| Lighter shade | #FFB499 rgb(255, 180, 153) hsl(15.9, 100%, 80%) oklch(0.8343 0.0956 40.61) | Hover states, tinted panels, the pale end of a dark-mode ramp. The hue is untouched, so the result still reads as the same colour, which is what a family needs. | HSL still claims 100% saturation, but the perceived chroma has halved, from 0.19 to 0.10. A ramp built this way fades toward grey at the light end while every value in it insists it is fully saturated. |
| Darker shade | #992900 rgb(153, 41, 0) hsl(16.1, 100%, 30%) oklch(0.4544 0.1539 36.49) | Pressed states, borders, text that has to hold up on a light background. Dark steps keep more chroma than pale ones, which is why a deep orange stays orange. | Move HSL lightness up by 19.6 points and perceptual lightness climbs 0.13; move it down by 30.4 points and it falls 0.25. The size of an HSL edit says nothing about the size of the visual change. |
One orange written in every model, and then the same orange lightened and darkened by HSL lightness alone. Comparing the three steps is the quickest way to see which models describe what you see and which ones only describe what you typed.
The table writes one orange in every model, and they agree on what matters: a warm, saturated orange. The shade rows are where they part company. Both steps were made by moving nothing but HSL lightness, and HSL reports the same hue, 16°, and the same full saturation at every step. The eye disagrees. Lightening to 80% takes the perceived chroma from 0.19 to 0.10 and nudges the perceptual hue a few degrees toward yellow, which is why the pale step reads as peach rather than orange with the lights turned up.
OKLCH exists to make that behaviour predictable. Its lightness follows human vision, so a 0.05 step looks the same size at the dark end of a ramp as at the light end — something HSL lightness cannot promise, because 50% there is a position in a formula, not halfway to white. If you are generating a ramp or a dark mode, work in OKLCH and convert at the end.
Hex is a storage format, and a good one: exact, universal, accepted everywhere. What it cannot do is carry a relationship, which makes it a poor place to make decisions. Reason in HSL, build in OKLCH, ship hex — with the oklch() declaration written after it in the rule, so the browsers that understand it take over.
Questions
Colour, answered plainly
What contrast ratio do I actually need?
4.5:1 for body text at AA, 3:1 for large text (18.66px bold or 24px regular) at AA. AAA asks for 7:1 and 4.5:1 respectively. If your text is decorative, disabled, or part of a logo, the rules don't apply — for everything else, they do.
Why does my gradient look muddy in the middle?
Browsers interpolate gradients in sRGB by default, which passes through grey when you blend two saturated complementary colours. The fix is either to add a middle stop that carries the saturation through, or to avoid complementary pairs in a direct two-stop gradient.
Is HSL the right model to design in?
It's the right model to reason in — hue, saturation and lightness map onto how designers describe colour. It's a poor model for measuring perceived brightness. That's why the palette generator works in HSL but the contrast checker works in relative luminance.
Can I paste these colours into Figma?
Yes. Copy all gives you five hex codes, one per line, which pastes cleanly into Figma's colour styles, a Tailwind config, or a CSS custom property block.
Why does lightening a colour in HSL change what colour it looks like?
Because HSL treats saturation and lightness as independent numbers and the eye does not. Push lightness from 60% to 80% with saturation pinned at 100% and the colour keeps its hue code but loses half its chroma, so it reads as peach rather than as a brighter orange. The perceptual hue drifts as well, from 39° to 41° in the table above. A ramp built on HSL lightness looks washed out at the pale end while every value in it reports full saturation.
Is OKLCH safe to use in a stylesheet yet?
Yes, with a fallback. Safari shipped oklch() in 2022 and Chrome and Firefox followed in 2023, so current browsers all take it; the visitors who don't are on old phones and locked-down corporate installs. Write the hex first and the oklch() declaration after it in the same rule — a browser that doesn't understand the second line drops it and keeps the hex. If you prefer to be explicit, wrap the declaration in @supports (color: oklch(0 0 0)).
Does a hex code carry transparency?
Only in the eight-digit form. #FF6B3580 is that same orange at about half opacity, because the last pair is the alpha in hex and 80 is 128 out of 255. Four-digit shorthand works the same way: #FB48 unpacks to #FFBB4488. The contrast checker on this page ignores alpha, so if you are testing a translucent colour, flatten it against its background first.