CSS Colors

Color is one of the most fundamental tools CSS gives you for shaping how a page looks and feels. Every property that paints something on screen — text, backgrounds, borders, shadows, outlines — ultimately needs a color value, and CSS offers more than a dozen different notations for specifying one, from simple keywords to precise, perceptually-uniform color spaces. Understanding how these formats work, how the browser resolves and blends them, and which one to reach for in a given situation will make your stylesheets both more predictable and easier to maintain.

Overview: How Color Works in CSS

In the CSS specification, color values belong to a data type called <color>, the same way a number of pixels belongs to <length> or a percentage belongs to <percentage>. No matter which notation you write — a keyword like tomato, a hex code like #ff6347, or a function like rgb(255 99 71) — the browser parses it down to the same underlying representation: a set of color channel values plus an alpha (opacity) channel. The notation you choose is purely a convenience for you as the author; it makes no difference to rendering performance or the final pixels, only to readability and precision.

Historically, browsers only understood colors in the sRGB color space, the same narrow gamut used by most consumer displays for decades. The CSS Color Module Level 4 and 5 specifications expanded this significantly, adding perceptually-based spaces like lab(), lch(), oklab(), and oklch(), plus the generic color() function for addressing wide-gamut spaces like display-p3. Modern evergreen browsers (Chrome, Firefox, Safari, Edge) support all of these; older browsers only understand hex, named colors, rgb()/rgba(), and hsl()/hsla().

Alpha and Compositing

Every color in CSS can carry an alpha channel controlling opacity, from fully transparent (0 or 0%) to fully opaque (1 or 100%). When the browser paints an element whose background or border color has partial alpha, it does not simply draw that color — it performs alpha compositing, mathematically blending the translucent color with whatever has already been painted underneath it (an ancestor’s background, an image, or the page’s base color). This is why the exact same rgba() value can look like a different shade depending on what sits behind it, and why stacking two semi-transparent elements produces a visibly darker or richer color where they overlap.

Inheritance and the Cascade

Color values are ordinary property values, so they follow the normal cascade and inheritance rules. The color property is inherited by default — set it on a parent and, unless overridden, every descendant’s text uses it too. background-color, border-color, and most other color-related properties are not inherited; each element defaults to its own initial value (typically transparent) unless you set it explicitly.

Syntax

The general shape of any color-using declaration is a property paired with a <color> value:

selector {
  property: <color-value>;
}
Format Example Notes
Named keyword rebeccapurple 147 predefined names; easiest to read, least precise
Hex (3/6 digit) #f60, #ff6600 Red/Green/Blue in hexadecimal, no alpha
Hex with alpha (4/8 digit) #f60a, #ff6600aa Adds an alpha channel as the last digit(s)
rgb() / rgba() rgb(255 102 0 / 0.7) Red, Green, Blue 0–255 (or %), optional alpha after /
hsl() / hsla() hsl(24 100% 50% / 0.7) Hue in degrees, Saturation and Lightness as %
hwb() hwb(24 0% 0%) Hue, Whiteness, Blackness — intuitive for tints/shades
lab() / lch() lch(60% 80 40) Perceptually uniform, device-independent
oklab() / oklch() oklch(70% 0.15 40) Newer, more perceptually accurate revision of lab/lch
currentColor border-color: currentColor; Resolves to the element’s computed color value
transparent background-color: transparent; Fully transparent black; the default for most backgrounds

In every function-based format, CSS Color 4 allows either the legacy comma-separated syntax (rgb(255, 102, 0)) or the modern space-separated syntax with an optional alpha after a slash (rgb(255 102 0 / 0.7)) — but never mix the two styles within a single function call.

Examples

Example 1: Keywords and hex values on a simple card

Applied to a <div class="card"> containing a heading and paragraph:

.card {
  background-color: #1e293b;
  color: #f8fafc;
  border: 2px solid slategray;
  padding: 1.5rem;
  border-radius: 8px;
}

Result: The card renders with a dark navy-slate background, near-white text for strong contrast, and a medium gray-blue 2px border framing the whole box.

This is the most common starting point: hex codes give you exact, reusable color values (useful for matching a brand palette), while the named keyword slategray is easier to read for a color that doesn’t need pixel-perfect precision. Note that color is inherited, so the heading and paragraph inside .card automatically pick up the near-white text color without repeating the declaration.

Example 2: Transparency with rgba() and hsla()

.badge {
  background-color: rgba(220, 38, 38, 0.85);
  color: white;
  border: 1px solid hsl(0, 70%, 30%);
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
  display: inline-block;
}

.overlay {
  position: absolute;
  inset: 0;
  background-color: hsla(220, 90%, 20%, 0.6);
}

Result: The .badge renders as a rounded red pill that is mostly opaque but lets a hint of whatever is behind it show through, with a darker red-brown 1px border. The .overlay, when stacked over other content via position: absolute, tints everything beneath it with a translucent dark blue — the underlying content remains visible but visually muted, a common pattern for modal backdrops and image captions.

Both examples use an alpha channel below 1, so the browser composites the color against its backdrop rather than painting it as a flat, opaque fill. This is the key mechanical difference between a translucent color and a fully opaque one: the final pixel color depends on layering, not just the declared value.

Example 3: Custom properties, oklch(), and color-mix()

:root {
  --brand: oklch(65% 0.18 250);
}

.button {
  background-color: var(--brand);
  color: white;
  border: 2px solid currentColor;
  padding: 0.6rem 1.25rem;
  border-radius: 6px;
}

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

Result: The button displays with a vivid medium blue background (defined once as the custom property --brand using the perceptually uniform oklch() function), white text, and a white border (since currentColor resolves to the button’s color, which is white). On hover, the background darkens smoothly toward black without you having to hand-pick or maintain a second hex value.

This pattern — storing a brand color once in a custom property, then deriving variations with color-mix() — is a hallmark of modern CSS color work. It keeps a theme’s palette in one place and lets hover, focus, and disabled states stay mathematically consistent with the base color instead of drifting out of sync over time.

Under the Hood: How the Browser Resolves and Composites Color

When the rendering engine encounters a color value, it works through roughly these steps:

  • Tokenizing and parsing: the value is recognized as a color token — a hash token for hex, an identifier for a keyword, or a function token like rgb(...) — and validated against the grammar for that format (right number of arguments, valid ranges).
  • Computing channel values: keywords and hex codes are converted to numeric channel values; functions like hsl() or oklch() are mathematically converted into the browser’s internal color representation.
  • Resolving special keywords: currentColor is resolved to the element’s own computed color value at this stage — it is dynamic and re-resolves if color changes (for example, on :hover).
  • Cascade and inheritance: as with any property, the browser determines the winning declaration by specificity and source order, then, for inheritable properties like color, propagates the computed value down to descendants that don’t set their own.
  • Painting and compositing: at paint time, if the alpha channel is less than 1, the engine blends the color’s RGB channels with the backdrop using the standard source-over compositing formula, layer by layer, from the bottom of the stacking context up.
  • Interpolation for transitions and gradients: when animating a color change or rendering a gradient, the browser interpolates between two colors in a specified (or default) color space. Interpolating in oklch or lch tends to produce smoother, more even-looking transitions than interpolating in raw rgb, because those spaces are designed so that equal numeric steps correspond to roughly equal perceived changes in color.

Common Mistakes

Mistake 1: Mixing legacy comma syntax with the modern alpha slash

CSS Color 4 supports two syntaxes for rgb()/hsl() — comma-separated or space-separated with a slash before alpha — but they cannot be combined in the same function call:

.alert {
  background-color: rgba(255, 0, 0 / 0.5);
}

This mixes commas between the RGB channels with a slash before the alpha value, which is invalid and will be ignored by the browser, silently falling back to the property’s inherited or initial value instead of the intended red. Pick one syntax consistently:

.alert {
  background-color: rgba(255, 0, 0, 0.5);
}

Mistake 2: Hardcoding low-contrast text without checking accessibility

It’s easy to reach for a color that looks fine to the author but fails accessibility guidelines for readers with low vision:

.notice {
  color: #999999;
  background-color: #ffffff;
}

Light gray text (#999999) on a white background produces a contrast ratio of roughly 2.8:1, well below the WCAG AA minimum of 4.5:1 for normal body text. This is syntactically valid CSS but a real usability defect. Darkening the gray restores sufficient contrast while keeping a muted, secondary-text feel:

.notice {
  color: #4b5563;
  background-color: #ffffff;
}

Best Practices

  • Store your palette in :root custom properties (--brand, --surface, --text-muted) so every color is defined once and themed consistently.
  • Prefer rgb()/hsl() functions over hex when you need transparency, since the alpha argument is explicit and easy to scan; use hex for solid, exact brand colors.
  • Use currentColor for borders, icons, and shadows that should always match nearby text — it stays correct automatically if the text color changes via theming or state.
  • Always check contrast ratios (aim for at least 4.5:1 for normal text, 3:1 for large text) when choosing text-on-background color pairs.
  • Don’t rely on color alone to convey meaning (error vs. success, required vs. optional) — pair it with an icon, label, or pattern for colorblind users.
  • Reach for oklch() or color-mix() when you need to programmatically lighten, darken, or blend brand colors instead of maintaining a hand-picked shade for every state.
  • Never mix comma and slash syntax within the same color function — pick one syntax and use it consistently across the codebase.

Practice Exercises

Exercise 1: Create a .tag class with a solid background color, white text, and a border that uses currentColor so it always matches the text. Change the text color and confirm the border updates automatically.

Exercise 2: Define a custom property --primary using oklch(), then write a :hover rule for a button that uses color-mix() to blend 15% black into --primary for a darker hover state.

Exercise 3: Given a paragraph with color: #767676 on a #ffffff background, determine whether it meets the WCAG AA contrast threshold of 4.5:1, and if not, adjust the hex value until it does.

Summary

  • Every CSS color, regardless of notation, resolves to the same internal channel-plus-alpha representation before painting.
  • Common formats include named keywords, hex (with optional alpha), rgb()/hsl()/hwb(), and the newer perceptually-uniform lab()/lch()/oklab()/oklch().
  • Colors with alpha below 1 are alpha-composited against their backdrop at paint time, not painted as a flat opaque fill.
  • color is inherited by default; most other color properties, like background-color, are not.
  • currentColor dynamically resolves to an element’s computed color value, making it useful for borders and icons that should track text color.
  • Never mix comma and slash syntax within one color function — it produces an invalid value that the browser discards.
  • Always verify text/background contrast for accessibility; a syntactically valid color can still be a usability defect.