Skip to content

Colours and backgrounds

docs.scrimba.com

You want a button to be a specific shade of blue. You find the colour in a design file, paste #1b2130 into your stylesheet, and it works, but you have no idea what those characters mean or how you would nudge the shade a little lighter. Colour in CSS looks like a pile of cryptic codes until you learn the few systems underneath them. Once you can read a colour and paint it onto a background, most of the visual character of a page is in your hands.

Naming a colour

CSS lets you write the same colour in a few different ways. The simplest is a keyword: a plain English name the browser already knows.

css
.tag {
  color: rebeccapurple;      /* a named colour, no codes needed */
}

There are about 140 of these names, which is handy for quick work but far too few for real designs. The other ways let you describe any colour at all. A hex code is a # followed by six characters, and rgb() mixes red, green, and blue by amount. They all point at exact colours; keywords are the shortcut for the common ones.

css
.tag {
  color: #1b2130;            /* the same dark navy, written as a hex code */
}

There are four ways to write a colour in CSS, and they describe the same colours through different systems. A keyword like rebeccapurple is a fixed name. A hex code like #1b2130 packs red, green, and blue into six characters. The rgb() function sets those same three channels as numbers from 0 to 255. And hsl() describes a colour by hue, saturation, and lightness instead.

css
.badge {
  color: #1b2130;                 /* hex: two chars each for red, green, blue */
  color: rgb(27, 33, 48);         /* the same colour as red, green, blue amounts */
  color: hsl(219, 28%, 15%);      /* the same colour as hue, saturation, lightness */
}

All three of those lines paint an identical dark navy. Reach for hsl() when you are choosing colours by hand. It reads like a description a person can reason about, where hex and rgb are numbers you mostly copy and paste.

CSS accepts a colour as a keyword, a hex code, rgb(), or hsl(), and the first three all address the same sRGB space (the standard colour space screens have used for decades) through different notations. A hex code is base-16: #1b2130 is red 1b, green 21, blue 30, each pair a value from 00 to ff (0 to 255 in decimal). rgb() writes those same three channels in plain decimal. Neither is built for a human to reason about, because moving a colour "a bit lighter" means adjusting three numbers at once with no clear direction.

css
.badge {
  color: #1b2130;               /* base-16: 1b red, 21 green, 30 blue */
  color: rgb(27 33 48);         /* same channels, decimal, modern space-separated syntax */
  color: hsl(219 28% 15%);      /* hue 219deg, 28% saturation, 15% lightness */
}

Prefer hsl(): its three axes each map to one thing you can adjust in isolation. Hue is a position from 0 to 360 on the colour wheel, saturation is how vivid the colour is, and lightness is how bright. Want a hover state a shade darker? Drop the lightness a few percent and leave the other two. That single readable axis is why hsl() composes cleanly with the custom properties you use to build a palette, where you might keep one hue and vary only lightness across a set of shades.

JunoNaming a colour The same colour can be written as a keyword like rebeccapurple, a hex code like #1b2130, or with rgb(). Keywords are quick but there are only about 140 of them, so most real work uses the codes. Do not try to read a hex code by eye yet; copying exact values is completely normal at this stage.
JunoNaming a colour Keyword, hex, rgb(), and hsl() all name the same colours through different systems. When you are picking colours yourself, use hsl(), because hue, saturation, and lightness are things you can actually reason about. Hex and rgb() are for values you copy from a design rather than tune by hand.
JunoNaming a colour Keyword, hex, and rgb() all address sRGB through different notations; hsl() reframes the same colour into three axes you can move one at a time. That is the whole reason to prefer it: a darker hover is one number, the lightness, instead of three coordinated ones. It pays off the moment you build a palette from custom properties and vary lightness across a scale.

Transparency

Sometimes you want a colour you can partly see through, so whatever sits behind it shows a little. This is transparency, and you add it with a fourth value called alpha: 0 is fully see-through, 1 is fully solid.

css
.overlay {
  background-color: rgb(27 33 48 / 0.5);   /* the navy, but half see-through */
}

The / 0.5 on the end means "half opacity". There is also a property called opacity that fades a whole element, text and all, at once:

css
.faded {
  opacity: 0.5;              /* fades the entire element, not one colour */
}

The difference matters: alpha affects only that one colour, while opacity fades everything about the element.

You make a colour see-through by adding an alpha value, where 0 is invisible and 1 is solid. Both rgb() and hsl() take it after a slash, and a hex code can carry it as two extra characters on the end.

css
.overlay {
  background-color: rgb(27 33 48 / 0.5);    /* half-transparent navy */
  background-color: hsl(219 28% 15% / 0.5); /* same thing via hsl */
  background-color: #1b213080;              /* same again: 80 is roughly 50% alpha */
}

There is an important distinction between alpha and the opacity property. Alpha makes one colour partly transparent and nothing else. opacity fades the entire element, including its text, its border, and any children, as a single unit. Fade one colour with alpha; fade a whole element with opacity. If your text goes faint when you only meant to soften a background, you reached for opacity where you wanted alpha.

Alpha is a fourth channel on a colour, from 0 (fully transparent) to 1 (fully opaque), and it belongs to that one colour value. Modern rgb() and hsl() take it after a slash, and hex accepts it as a fourth byte, so #1b213080 is the navy at 80/ff, roughly 50%. Because alpha lives on the colour, you can make a background translucent while text, borders, and everything else stays fully solid.

css
.overlay {
  background-color: hsl(219 28% 15% / 0.5);  /* only this fill is translucent */
  color: white;                              /* text stays fully opaque */
}

opacity is a different tool that happens to look similar. It sets the transparency of the element as a whole, and it does so by compositing the element and all its descendants onto the page as one group. Two consequences follow that alpha does not have. First, opacity below 1 establishes a new stacking context (a self-contained layer for how overlapping elements are ordered), which can change what sits on top of what. Second, you cannot recover a solid child inside a faded parent, because the fade applies to the finished group. When you only want one fill or one border to be see-through, use alpha on that colour and leave opacity alone.

JunoTransparency Add a fourth value to a colour to make it see-through: 0 is invisible, 1 is solid, and something like / 0.5 is halfway. That alpha only touches the one colour. The separate opacity property fades the whole element, text included, so pick alpha when you only want a softer background.
JunoTransparency Alpha is transparency on a single colour, written after a slash in rgb() or hsl(), or as two extra hex characters. The opacity property is the blunt version: it fades the entire element and its children at once. When your text unexpectedly goes faint, you used opacity where alpha was the right call.
JunoTransparency Alpha lives on the colour, so a translucent fill leaves solid text and borders untouched. opacity composites the whole element and its descendants as one group, which is why a child cannot stay solid inside a faded parent, and why it quietly spins up a new stacking context. Reach for alpha unless you actually want the entire element to fade together.

Background colour and images

The background-color property fills the area behind an element, out to its border. You can use any colour you can name.

css
.hero {
  background-color: hsl(219 28% 15%);   /* a solid navy behind the content */
}

You can also put an image behind an element with background-image, pointing at the file with url(...):

css
.hero {
  background-image: url("mountains.jpg");
  background-size: cover;               /* scale the image to fill the whole area */
}

background-size: cover scales the picture so it fills the box completely, cropping any overflow. It is the setting you want most of the time so there are no empty gaps.

background-color paints the area behind the content, filling the padding box out to the border's edge. On top of that colour you can layer an image with background-image and url(...). Because it layers on top, a background colour underneath an image shows through any transparent parts and while the image is still loading.

css
.hero {
  background-color: hsl(219 28% 15%);   /* shows through and loads first */
  background-image: url("mountains.jpg");
  background-size: cover;               /* fill the box, crop the overflow */
  background-position: center;          /* keep the middle of the image in view */
  background-repeat: no-repeat;         /* one copy, do not tile */
}

Three properties control how the image sits. background-size is usually cover (fill the box, crop what spills over) or contain (fit the whole image inside, which can leave gaps). background-position decides which part stays in view when the image is cropped, center being the safe default. background-repeat: no-repeat stops a small image tiling across the box.

background-color fills the element's background area, which by default is the padding box, so the colour reaches the outer edge of the padding and stops at the border. background-image paints on top of that colour, and it is worth keeping a solid background-color underneath any photographic image: it is the fallback shown while the image loads or if the request fails, and it shows through transparent regions, so choosing one close to the image's tone avoids a flash of bare page.

css
.hero {
  background-color: hsl(219 28% 15%);   /* fallback and load-time fill */
  background-image: url("mountains.jpg");
  background-size: cover;               /* cover: fill box, crop overflow */
  background-position: center;          /* anchor the crop on the centre */
  background-repeat: no-repeat;
}

The sizing choice is a real trade-off. cover scales the image to fill the box on both axes and crops whatever spills past, so nothing is left blank but part of the image is lost; contain scales until the whole image fits, so nothing is cropped but the box can show uncovered strips (the background-color fills those). background-position matters most under cover, because it decides which region survives the crop: center is safe, but a portrait might want top to keep a face in frame. For crisp non-photographic fills, note that a gradient counts as a background-image too, which the next section uses.

JunoBackground colour and imagesbackground-color fills the space behind an element, and background-image: url(...) puts a picture back there instead. Pair an image with background-size: cover so it fills the whole box without leaving gaps. That combination covers most of what you will reach for early on.
JunoBackground colour and images A background image layers on top of the background colour, so keep a colour underneath for while the image loads. cover fills the box and crops the overflow, contain fits the whole image and may leave gaps, and background-position decides which part stays visible when cropped. Add no-repeat unless you actually want the image to tile.
JunoBackground colour and images Keep a background-color under any photo: it is the fallback during load and shows through transparent areas. cover versus contain is fill-and-crop versus fit-and-maybe-gap, and background-position only earns its keep under cover, where it chooses what survives the crop. Anchor on center by default and move it only when a specific part of the image has to stay in frame.

Gradients

A gradient is a smooth blend from one colour to another. CSS treats it as a kind of image, so it goes in background-image rather than background-color.

css
.banner {
  background-image: linear-gradient(hsl(219 28% 15%), hsl(219 40% 35%));
}

That blends from the first colour at the top down to the second at the bottom. You can list more than two colours, and you can change the direction:

css
.banner {
  background-image: linear-gradient(to right, hsl(340 80% 55%), hsl(30 90% 60%));
  /* blends left to right instead of top to bottom */
}

No image file needed; the browser draws the blend for you.

A gradient is a generated image, which is why linear-gradient() goes in background-image and not background-color. Its first argument is the direction, then a list of colours to blend between. Leave the direction out and it runs top to bottom.

css
.banner {
  background-image: linear-gradient(
    to right,                    /* direction: left edge to right edge */
    hsl(340 80% 55%),            /* start colour */
    hsl(30 90% 60%)              /* end colour */
  );
}

radial-gradient() works the same way but blends outward from a centre point in rings, rather than along a straight line. It is the tool for a soft glow or a spotlight effect.

css
.spotlight {
  background-image: radial-gradient(hsl(0 0% 100%), hsl(219 28% 15%));
  /* bright centre fading out to dark edges */
}

Because a gradient is a background image, it stacks with real images and with other gradients under the same background-image property, separated by commas.

A gradient is a generated image, so it lives in background-image and obeys the same sizing and layering rules as a url(...). linear-gradient() takes an optional direction first (to right, or an angle like 135deg), then a list of colour stops it blends between. Give a stop an explicit position and you control exactly where a colour lands, which turns a gradient into a hard stripe when two stops meet at the same point.

css
.banner {
  background-image: linear-gradient(
    135deg,                      /* angle: 0deg is up, 90deg is right */
    hsl(340 80% 55%) 0%,
    hsl(30 90% 60%) 100%
  );
}

radial-gradient() blends from a centre outward and takes an optional shape and size before its stops. Both are images in every sense, so they layer under one comma-separated background-image, earlier gradients painting over later ones, which is how a translucent gradient becomes a tint over a photo:

css
.hero {
  background-image:
    linear-gradient(hsl(219 28% 15% / 0.6), hsl(219 28% 15% / 0.6)),  /* dark tint on top */
    url("mountains.jpg");                                             /* photo underneath */
  background-size: cover;
}

Because hsl() exposes lightness directly, a two-stop gradient between the same hue at two lightness values gives a clean, controllable fade, which is one more place choosing hsl over hex pays off.

JunoGradients A gradient is a smooth blend between colours, and CSS counts it as an image, so it goes in background-image. linear-gradient() blends in a straight line, top to bottom by default, or add to right to change direction. No file needed, the browser draws the blend itself.
JunoGradients Both linear-gradient() and radial-gradient() are generated images, so they belong in background-image. Linear blends along a line you aim with a direction; radial blends outward from a centre in rings, good for a glow. Because they are images, you can stack several under one comma-separated background-image.
JunoGradients Gradients are images, so they take positions, layer under one background-image, and obey background-size. Give colour stops explicit positions for precise fades or hard stripes, and stack a translucent gradient over a url(...) to tint a photo without a second element. A two-stop hsl() fade between the same hue at two lightness values is the cleanest kind to control.

Colour that adapts and stays readable

One useful keyword is currentColor. It stands in for whatever the element's text colour is, so a border or icon can match the text automatically:

css
.chip {
  color: hsl(219 28% 15%);
  border: 2px solid currentColor;   /* the border matches the text colour */
}

Change the text colour later and the border follows on its own, with no second edit.

One more thing to keep in mind: make sure text stands out from its background. Dark text on a dark background is hard to read, and pale grey on white strains the eyes. Aim for a clear difference in brightness between the two.

currentColor is a keyword that resolves to the element's own color value, so borders, outlines, and SVG icons can track the text colour without repeating it. Set the text once and everything tied to currentColor updates with it.

css
.chip {
  color: hsl(219 28% 15%);
  border: 2px solid currentColor;   /* follows color, no duplicate value */
}

The property to watch alongside this is contrast: the difference in brightness between text and its background. Low contrast is hard to read, and accessibility guidelines set a minimum ratio for body text. This is where hsl() earns its place again: because lightness is its own axis, you can keep a hue and push the text and background lightness far enough apart to stay legible, without guessing at three channels at once.

currentColor resolves to the computed color of the element, which makes it a lightweight way to tie borders, outlines, box shadows, and SVG fill to the text colour. It inherits like any value, so setting color on a container cascades a single source colour down to every descendant that references currentColor, which is a clean pattern for theming a component from one declaration.

The harder constraint is contrast, the measured difference in relative luminance between foreground and background. The accessibility standard, WCAG (the Web Content Accessibility Guidelines), sets a minimum ratio of 4.5 to 1 for normal body text and 3 to 1 for large text, and text below that is hard to read for many people, not only those with low vision. hsl() helps you hit it: lightness is a separate axis, so you can hold a hue and saturation and move only the two lightness values apart until the pair passes, rather than nudging six channels blind. It is an aid to reasoning, not a guarantee, since equal-lightness colours can still fail the true luminance calculation, so verify the final ratio with a checker.

Two more things are worth knowing at this level. The background shorthand sets many background properties at once, and its one rule is that background-size comes after background-position, separated by a slash: background: url("bg.jpg") center / cover no-repeat. And CSS now has colour functions beyond hsl(), such as oklch(), that reach colours outside the sRGB space on displays that support a wider gamut (the range of colours a screen can show). You do not need them yet, but knowing the newer functions exist means you will recognise them when a design system reaches for a more vivid colour than sRGB can express. Pair this with readable typography and the text carries its meaning to every reader.

JunoColour that adapts and stays readablecurrentColor is a keyword that copies the element's text colour, so a border can match the text and follow it if you change it later. Beyond that, keep text readable: make sure it is clearly brighter or darker than what sits behind it. Low contrast is the most common reason a page is hard to read.
JunoColour that adapts and stays readablecurrentColor ties borders, outlines, and icons to the text colour so you set it in one place. Watch contrast, the brightness gap between text and background, because accessibility rules set a minimum for body text. Pulling the lightness values apart in hsl() is the reliable way to keep that gap wide enough.
JunoColour that adapts and stays readablecurrentColor inherits, so one color on a container themes every descendant that references it. WCAG wants 4.5 to 1 for body text, and hsl() lets you move only lightness to reach it, though equal lightness can still fail the real luminance check, so verify with a tool. Remember the background shorthand puts size after position with a slash, and newer functions like oklch() exist for wider gamuts when a design needs them.