Guide
How to use color codes in CSS
Every way CSS accepts a colour, and the two features that mean you should almost never repeat a hex code.
CSS will take a colour in about a dozen notations, and they are all equally valid. The more useful question is where to write it, because a stylesheet with the same hex code in forty places cannot be rebranded.
The notations, in one place
| Form | Example | Use it for |
|---|---|---|
| Hex | #3498db | Carrying a value from a design tool |
| Hex with alpha | #3498db80 | Translucency without switching notation |
rgb() | rgb(52 152 219 / 50%) | Channel arithmetic, or when a value comes from code |
hsl() | hsl(204 70% 53%) | Hand-adjusting lightness or building a ramp |
oklch() | oklch(62% 0.13 240) | Perceptually even ramps, wide-gamut colours |
| Keyword | tomato | Prototypes and debugging only |
currentColor | border-color: currentColor | Anything that should follow the text colour |
transparent | background: transparent | Explicitly no colour |
Note the modern space-separated syntax: rgb(52 152 219 / 50%) rather than rgba(52, 152, 219, 0.5). The comma forms still work and always will, but the space form is what current specs use, and it removes the need for a separate rgba() function.
Custom properties are the actual answer
A hex code in a rule is a value. A hex code in a custom property is a decision you can change later. The difference compounds fast.
The mistake most people make on the first attempt is naming properties after the colour:
--blue: #3498db; works until the brand blue becomes green six months later. Then you have a property called --blue holding a green, or a rename touching two hundred files.
Name them after the job instead. Two layers works well:
Layer one, the ramp. Literal values, named by family and step: --blue-500: #3498db. These are ingredients and are never used directly in a component.
Layer two, the roles. Aliases that say what a colour is for: --text-body, --surface-card, --border-subtle, --action-primary. Components only ever reference these.
The payoff is that a theme becomes a small block of overrides. Dark mode leaves the ramp alone and reassigns the roles:
:root { --surface-card: var(--grey-0); }[data-theme="dark"] { --surface-card: var(--grey-900); }
Every component that used var(--surface-card) is now theme-aware and you did not edit any of them. This site is built exactly this way.
Relative colours replace most colour functions
CSS can now derive one colour from another, which used to require Sass. The relative colour syntax reads a source colour and lets you rewrite individual channels:
--hover: oklch(from var(--brand) calc(l - 0.06) c h);
That says: take the brand colour, drop its lightness by six percent, keep its chroma and hue. It updates automatically if the brand colour changes, and it works with runtime values that a preprocessor could never see.
color-mix() covers the other common case, blending toward another colour:
--tint: color-mix(in oklab, var(--brand) 12%, white);
Mix in oklab rather than the default sRGB. Sass's mix() worked in sRGB, which is why its midpoints often came out muddy. The colour mixer shows the two side by side.
Alpha, and the trap in it
Using opacity for a muted text style is tempting and usually a mistake. Text at rgb(0 0 0 / 60%) has a different contrast ratio on every background it might land on. If the component appears on both a white card and a tinted surface, it passes in one place and fails in the other.
Prefer a solid colour from your ramp for text. Reserve alpha for things that genuinely need to show what is behind them: scrims, overlays, hover washes, focus rings.
Also keep opacity and a colour's alpha distinct in your head. opacity applies to the whole element including its children and creates a new stacking context; a colour's alpha applies to that one paint operation.
A short list of things worth doing
- Declare colours once, as custom properties, in two layers.
- Name roles, not hues.
- Use
currentColorfor icon fills and borders that should follow the text. - Set
color-scheme: light darkso form controls and scrollbars follow your theme. - Mix and interpolate
in oklab. - Check the pairs, not the values. A ramp that looks tidy can still fail contrast, so run the combinations you actually ship through the contrast checker.
If a colour is currently trapped in an image or a screenshot, the image picker will get you the exact value, and the converter will give it to you in whichever notation your stylesheet uses.