CSS gives you more ways to write a color than any project needs. That is fine — most of them exist for good reasons, and knowing which one to reach for saves a lot of small decisions. This guide covers the formats, the case for custom properties, how gradients actually work, and what the newer color functions change.

The formats, and when each is the right call

HEX

#3B82F6. Six hex digits for red, green and blue. Universally supported, compact, and the format every design tool copies to your clipboard. Three-digit shorthand (#F80) expands by doubling each digit. Eight digits adds alpha: #3B82F680 is that blue at about 50%.

Use it for storage and for pasting between tools. Its weakness is legibility — nobody reads #3B82F6 and knows it is a mid blue without looking.

rgb()

Modern CSS accepts space-separated values with a slash for alpha: rgb(59 130 246 / 50%). The older comma form and the separate rgba() function still work and always will, but the new syntax is what current specs favour.

The reason to reach for rgb() is transparency composed from parts — particularly with custom properties, covered below.

hsl()

hsl(217 91% 60%). Hue in degrees, saturation and lightness as percentages, with the same slash-alpha syntax. This is the readable format: you can look at a stylesheet full of HSL values and see the relationships between them.

If you write a scale by hand, HSL is the format to write it in, because a ramp becomes one number changing down a column. The HEX to RGB converter and RGB to HSL converter handle the translation when a brand hands you a HEX.

Named colors

148 keywords — rebeccapurple, tomato, cornflowerblue. Genuinely useful for prototypes and debugging (outline: 2px solid magenta is a fine temporary marker). They have no place in a design system, because they are not on any scale and cannot be adjusted.

transparent and currentColor are the two keywords that matter in production. currentColor resolves to the element's computed color, which makes it excellent for icons and borders that should follow their text automatically.

Custom properties: the actual upgrade

Everything above is a spelling choice. Custom properties change how a stylesheet is structured, and that is the difference that matters.

:root {
  --brand-500: #3B82F6;
  --brand-600: #2563EB;
  --text: #0B0F14;
  --surface: #FFFFFF;
}

.button { background: var(--brand-500); color: var(--surface); }
.button:hover { background: var(--brand-600); }

One definition, many references. A brand change becomes one edit rather than a find-and-replace across a codebase — the kind that always misses a nearly-identical shade in a component nobody remembered.

Two techniques are worth knowing beyond the basics.

Store channels, not colors, when you need alpha. You cannot apply transparency to a HEX custom property directly. Store the channel numbers instead:

:root { --brand-rgb: 59 130 246; }

.overlay { background: rgb(var(--brand-rgb) / 12%); }
.ring    { box-shadow: 0 0 0 3px rgb(var(--brand-rgb) / 40%); }

Now one variable produces the solid color and every translucent version of it. The CSS color generator outputs both forms for any color you give it.

Theme by redefining, not by overriding. Because custom properties cascade, a theme is a second set of values on a different selector:

:root { --surface: #FFFFFF; --text: #0B0F14; }

[data-theme="dark"] { --surface: #0B0F14; --text: #E7ECF2; }

No component CSS changes. Every rule that references var(--surface) follows automatically. This site's own theme toggle works exactly this way.

A note on naming: semantic names (--surface, --text-muted, --border) survive redesigns far better than literal ones (--light-grey). The moment --light-grey needs to be dark in dark mode, the name is a lie. Many teams keep both layers — a literal scale like --blue-500, and semantic aliases that point at it.

Gradients

A gradient is an image, not a color, so it belongs in background rather than background-color.

.hero {
  background: linear-gradient(135deg, #3B82F6 0%, #A855F7 100%);
}

The angle runs clockwise from 0deg pointing up, so 90deg goes left-to-right and 135deg runs diagonally down-right. Keywords like to bottom right are equivalent and often clearer.

radial-gradient() blends outward from a point, and conic-gradient() sweeps around one — conic is what makes pie charts and color wheels possible in pure CSS.

Two practical points. First, gradients between distant hues can pass through a muddy middle, because interpolation runs through a straight line in the color space and that line may dip toward grey. Adding an intermediate stop in a related hue usually fixes it, and the color mixer is a quick way to find one. Second, a transition to transparent can produce a grey haze in older engines, because transparent historically meant transparent black. Fade to an explicit zero-alpha version of the same color instead: rgb(59 130 246 / 0).

The gradient generator produces copy-ready CSS with a live preview, which is faster than guessing angles.

Modern color functions

Several newer features have landed across current browsers. They are worth knowing, with the usual caveat that you should check your own analytics before depending on any of them.

color-mix() blends two colors in a space you choose:

.button:hover { background: color-mix(in oklab, var(--brand) 85%, black); }

This is a hover state derived from the brand color rather than hard-coded, which means it tracks a brand change automatically.

oklch() and oklab() are perceptually uniform spaces. In OKLCH — lightness, chroma, hue — equal lightness numbers look equally light regardless of hue, which is exactly the thing HSL gets wrong. A palette built with a fixed OKLCH lightness across hues is far more consistent than the HSL equivalent, and it is why several design systems have migrated.

Relative color syntax derives one color from another:

--brand-dark: hsl(from var(--brand) h s calc(l - 15%));

Support here is newer than the others; treat it as progressive enhancement and keep a static fallback.

A pragmatic approach: author your scales with a tool, output plain HEX or rgb() custom properties for maximum compatibility, and use color-mix() or OKLCH where a derived value genuinely saves maintenance. The Tailwind color generator and Bootstrap color generator both emit conventional values that work everywhere.

Framework conventions

Tailwind expects each color as a 50–950 scale, so a single brand HEX is never enough on its own. The generator builds the full ramp and a config block to paste.

Bootstrap uses both Sass variables and CSS custom properties, and notably needs an RGB channel triplet (--bs-primary-rgb) to build its translucent backgrounds. If you set only the solid color, the translucent variants keep the old hue — a confusing bug with a simple cause.

Things that go wrong

Setting color but not background-color. If a user's browser or extension applies a different background, your text may become unreadable. Set both together whenever you set either.

Forgetting ::selection and ::placeholder. These inherit defaults that often fail contrast against a custom theme, and almost nobody checks them.

Focus rings that only work on one background. Removing the default outline without a solid replacement is one of the most common accessibility regressions in modern CSS. Use :focus-visible, and verify the ring against every surface it can land on.

Assuming a dark theme is an inversion. It is a separate palette that needs its own contrast checks — see the accessible color guide.

For the theory behind which format describes what, the color models guide covers the underlying spaces in more detail.