021: Colors and Backgrounds
Learning outcomes
By the end of this lesson, you should be able to choose foreground and background colors that remain readable, write colors with hex, named, rgb(), and hsl() notation, and add alpha without making text accidentally faint. You will also distinguish color from the background properties, build useful gradients and image backgrounds with fallbacks, and test contrast and decorative backgrounds in the situations where a page is actually used.
Prerequisites and retrieval
Open the portfolio stylesheet and retrieve the cascade rule you already know: which declaration wins between .project { color: blue; } and article.project { color: green; }? Also identify the text color that is inherited. The next step is to build a small palette without solving the problem by increasing selector specificity.
Terminology
- Color space/model: A method for mapping numeric components to colors, such as sRGB channels or hue/saturation/lightness. — Source: MDN: values
- Foreground: The paint applied to text and text decorations through the
colorproperty. — Source: MDN: values - Background layer: One comma-separated background painting layer, such as an image or gradient, painted above
background-color. — Source: MDN: Using multiple backgrounds - Alpha: An opacity channel ranging from 0 (fully transparent) to 1 (opaque). — Source: MDN: values
- Gradient: A generated image that transitions between two or more color stops. — Source: MDN: Applying color
- Contrast ratio: A comparison of the relative luminance of foreground and background,
(L1+0.05)/(L2+0.05). — Source: WCAG 2.2: Contrast Minimum - Fallback: A simpler earlier declaration that remains available when a later declaration fails or is still loading. — Source: CSSWG: CSS Syntax Level 3
- Hex color: "A hex color is a # followed by 3, 4, 6, or 8 hex digits (0-9, a-f), e.g., #1e3a8a." — Source: MDN: CSS — hex colors
- RGB (
rgb()): "rgb() defines a color using red, green, blue channels 0-255 or 0%-100%, with optional /alpha 0-1." — Source: MDN: CSS — rgb() - HSL (
hsl()): "hsl() defines a color by hue (0-360deg), saturation and lightness percentages, with optional /alpha." — Source: MDN: CSS — hsl()
Mental model: stacked paint with readable ink
An element is not painted as one undifferentiated color. Its background color sits below its background images, while its content is painted above those backgrounds. The color property supplies the current foreground color; text inherits it, and decorations often use it too. Transparent paint lets lower layers show through, which means contrast must be checked against the final composite rather than against an assumed white canvas.
These are the common formats you will encounter:
color: rebeccapurple; /* named, useful for demos */
color: #1e3a8a; /* hexadecimal sRGB */
color: rgb(30 58 138); /* modern sRGB channels */
color: rgb(30 58 138 / 80%); /* slash alpha */
color: hsl(224 64% 33%); /* hue, saturation, lightness */
Choose the notation your team can read and maintain. Hex is compact, rgb() makes channels explicit and gives alpha a readable form, and hsl() is often convenient when manually creating related variations. None of these formats, by itself, tells you whether the resulting pair is accessible.
Beginner example: a coherent portfolio palette
Begin with a neutral canvas, dark body text, one accent, and interaction states that are visible without relying on guesswork:
html { box-sizing: border-box; }
*, *::before, *::after { box-sizing: inherit; }
body {
margin: 0;
color: rgb(30 41 59);
background-color: rgb(248 250 252);
font-family: system-ui, sans-serif;
line-height: 1.6;
}
header {
color: white;
background-color: rgb(30 58 138);
padding: 2rem 1rem;
}
header p {
color: rgb(219 234 254);
}
.project {
margin-block: 1rem;
padding: 1rem;
color: rgb(30 41 59);
background-color: white;
border: 1px solid rgb(203 213 225);
border-inline-start: 0.35rem solid rgb(37 99 235);
}
a {
color: rgb(29 78 216);
text-decoration-color: rgb(29 78 216 / 55%);
text-underline-offset: 0.2em;
}
a:hover { text-decoration-color: currentColor; }
a:focus-visible {
outline: 3px solid rgb(249 115 22);
outline-offset: 3px;
}
Follow the inheritance here. The heading inside header gets white from its parent, while the paragraph receives a quieter blue-white from its own rule. currentColor refers to the computed color, so the hover underline stays coordinated with the link. Alpha is reasonable for a secondary underline, but it is a poor default for body text because the surrounding paint can make the result too faint.
Test this at high zoom and by moving through the page with the keyboard. Then temporarily switch the body background to a dark color. Transparent backgrounds and borders will expose how much they depend on the paint behind them.
Intermediate example: background layers and gradients
This hero should remain usable while its decorative image is downloading, unavailable, or blocked:
<header class="hero">
<p class="hero__eyebrow">Frontend portfolio</p>
<h1>Interfaces made clear</h1>
<p>I turn semantic HTML into resilient experiences.</p>
</header>
.hero {
color: white;
background-color: rgb(15 23 42);
background-image:
linear-gradient(120deg, rgb(15 23 42 / 95%), rgb(30 58 138 / 78%)),
url("images/workspace.jpg");
background-position: center;
background-size: cover;
background-repeat: no-repeat;
padding: 4rem 1rem;
}
.hero__eyebrow {
color: rgb(191 219 254);
text-transform: uppercase;
letter-spacing: 0.08em;
}
The comma-separated backgrounds are layers, and the first layer is painted closest to the viewer. Here the dark gradient protects the text from the unpredictable tones in the photograph. background-color is still doing useful work underneath: it is available while the file loads and when the request fails. Background images do not provide an accessible name, so keep this one decorative. If a portfolio screenshot communicates meaning, use <img> with suitable alt text instead.
A generated gradient can provide a section accent without downloading an image:
.section-title {
background-image: linear-gradient(90deg, rgb(37 99 235), rgb(124 58 237));
background-size: 4rem 0.25rem;
background-position: left bottom;
background-repeat: no-repeat;
padding-block-end: 0.6rem;
}
Decorative paint must not carry essential information. If every background is disabled, the heading should still be meaningful.
Optional advanced example: maintainable custom properties
Custom properties let a stylesheet give names to repeated design decisions:
:root {
--color-text: rgb(30 41 59);
--color-surface: white;
--color-canvas: rgb(248 250 252);
--color-accent: rgb(29 78 216);
--color-focus: rgb(249 115 22);
}
body { color: var(--color-text); background: var(--color-canvas); }
.project { background: var(--color-surface); }
a { color: var(--color-accent); }
a:focus-visible { outline-color: var(--color-focus); }
Name the role, not the current visual value. --color-accent can change to a different hue without becoming a misleading --blue. Keep the first palette small and understandable; custom properties centralize decisions, but they do not perform contrast testing for you.
Mistakes, debugging, and DevTools
- Setting only
background: inherited text may become unreadable. Treat foreground/background pairs together. - Using alpha on text: it blends unpredictably with layered backgrounds and often lowers contrast.
- Incorrect modern syntax: use
rgb(0 0 0 / 50%), not commas mixed with slash. - Wrong URL: CSS URLs are relative to the stylesheet, not the HTML document.
- Using
background-size: coverand expecting the whole image: cover fills the box and may crop edges. - Replacing content with a background image: it disappears in high-contrast settings, print, or failed loading and has no
alt. - Communicating status by color alone: add text or another visual form.
For a concrete diagnosis, inspect the element in DevTools and select its color swatch. The picker can show the rendered color and, where supported, contrast information. Toggle background layers individually. A failed image request appears in the Network panel, while Computed shows the final color and background-color. If your browser supports forced colors, simulate that mode and make sure controls are still identifiable.
Accessibility and performance
WCAG 2.2 sets a 4.5:1 minimum contrast ratio for normal text and 3:1 for large text, defined there as at least 18pt or 14pt bold. User-interface components and meaningful graphical objects generally need 3:1 against adjacent colors. Logos and incidental or inactive content have separate exceptions, but those exceptions are not a sensible design target.
Color alone should not communicate errors, selection, or required fields. Keep underlines, or provide another non-color cue, for inline links. A gradient over a photograph needs testing at several crop positions and viewport widths because both the image and the overlay relationship can change.
Compress photographs, provide dimensions for content images, and choose appropriate formats. A CSS gradient is usually inexpensive compared with a large bitmap, but avoid enormous off-screen backgrounds and repeated downloads. CSS backgrounds work well for decoration; meaningful images should use responsive HTML images for semantics and stronger loading controls.
Deep dive: color formats and alpha
CSS supports several color syntaxes. The practical choice is a project convention that stays readable for the people maintaining the stylesheet.
.example {
color: rebeccapurple; /* named */
border-color: #7c3aed; /* hex */
background-color: rgb(124 58 237); /* modern rgb */
outline-color: hsl(262 83% 58%); /* hsl */
}
Modern space-separated notation can carry an alpha value after a slash:
.overlay {
background: rgb(15 23 42 / 0.72);
}
Put alpha on the particular color when only that paint should be translucent. opacity applies to the entire element and therefore also affects its children.
/* Text also becomes translucent */
.card {
opacity: 0.6;
}
/* Only the background is translucent */
.card {
background: rgb(255 255 255 / 0.6);
}
Deep dive: background layers
A background may contain several images or gradients. As in the hero example, the first listed layer is closest to the viewer:
.hero {
background-image:
linear-gradient(rgb(15 23 42 / 0.75), rgb(15 23 42 / 0.75)),
url("/images/team.jpg");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
The useful mental picture is a stack of transparent sheets over the element's background color. The gradient creates a consistent contrast layer, and the photograph supplies the visual context underneath it.
The background shorthand is compact, but its combined values can be difficult to inspect. Learn the longhands first:
background-colorbackground-imagebackground-repeatbackground-positionbackground-sizebackground-attachmentbackground-originbackground-clip
Once the longhands are familiar, shorthand is fine when the intent remains obvious to the team.
Worked example: card palette with semantic tokens
:root {
--color-text: #0f172a;
--color-muted: #475569;
--color-surface: #ffffff;
--color-border: #cbd5e1;
--color-brand: #2563eb;
--color-brand-strong: #1d4ed8;
}
.card {
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
}
.card__meta {
color: var(--color-muted);
}
.card__action {
color: white;
background: var(--color-brand);
}
.card__action:hover {
background: var(--color-brand-strong);
}
Each custom property describes a role rather than an exact hue. A later theme can replace the palette without forcing a name such as --blue-500 to describe a color that is no longer blue.
Worked example: layered decorative gradient without harming content
<section class="promo">
<div class="promo__content">
<p class="eyebrow">New course</p>
<h2>Build responsive interfaces</h2>
<a href="/courses/css">View course</a>
</div>
</section>
.promo {
min-block-size: 18rem;
display: grid;
align-items: end;
padding: 2rem;
color: white;
background:
linear-gradient(
to top,
rgb(15 23 42 / 0.92),
rgb(15 23 42 / 0.2) 65%
),
url("/images/css-course.jpg") center / cover no-repeat;
}
The copy is still real HTML, so it remains available to browsers, assistive technology, print, and users who cannot load the image. The image is decorative background content. If the image itself contains essential information, use <img> and provide appropriate alternative text.
Deep dive: gradients are generated images
These gradient families cover several common jobs:
.linear {
background: linear-gradient(135deg, #2563eb, #7c3aed);
}
.radial {
background: radial-gradient(circle at top left, #fef3c7, #ffffff 60%);
}
.conic {
background: conic-gradient(#2563eb 0 25%, #16a34a 25% 60%, #f59e0b 60%);
}
A conic gradient can make a chart-like shape or a decorative effect, but color alone is not a sufficient data encoding. A real visualization also needs labels and accessible textual equivalents.
Color-debugging checklist
When a palette does not look right, work through the actual rendered states:
- Is foreground/background contrast sufficient in every state?
- Did
opacityaccidentally dim child text? - Is a background image failing to load, exposing an unreadable fallback?
- Are hover/focus/disabled states still distinguishable?
- Does the design work in high zoom and forced-color environments?
- Is meaning communicated by more than color alone?
Color is part of the interaction system, not merely surface decoration.
Background attachment: use with restraint
.hero {
background-image: url("/images/landscape.jpg");
background-attachment: scroll;
}
scroll is the normal behavior. fixed anchors the background to the viewport and can create a parallax-like effect:
.hero {
background-attachment: fixed;
}
Do not expect fixed to behave identically on every mobile browser, and never make the effect necessary for understanding the page. Large fixed backgrounds can also cost more to paint.
Background-position and focal points
.hero {
background-position: 70% 35%;
}
Adjusting the position helps when the subject is off-center. Re-test across aspect ratios: a position that keeps a face visible on a desktop crop may cut it off on a narrow screen.
Tiered exercises
Checkpoint: test the palette as a system
Build a small matrix with body text, muted text, links, focus, card borders, and hero text. Check each foreground against the background it actually sits on. Include visited, hover, focus, and disabled states where they exist. A palette is not finished because five swatches look harmonious in isolation; it is finished when its states remain readable and distinguishable.
Disable the hero image, then disable the gradient. The color fallback should keep the content understandable during loading and failure. Finally, view the page in grayscale or with a color-vision simulation and confirm that links, errors, and selection still have a cue besides hue.
Record decisions by role and state: canvas, surface, primary text, muted text, accent, visited link, focus, success, and error. A project may not need every role, but naming the roles it does use makes inconsistent duplicates easier to find. Check muted text especially: reduced emphasis must not mean barely visible.
Foundation: Give the portfolio a neutral canvas, readable text, white project surfaces, and one accent color. Write one color each as hex, rgb(), and hsl().
Core: Create a dark hero with a color fallback, photo, and contrast-protecting gradient. Add link hover and keyboard focus states that are not color-only.
Stretch: Convert repeated palette values to role-based custom properties. Verify normal text contrast with a browser tool or trusted contrast checker and record the ratio.
:root {
--text: rgb(30 41 59);
--canvas: #f8fafc;
--surface: hsl(0 0% 100%);
--accent: rgb(29 78 216);
--focus: rgb(249 115 22);
}
body { color: var(--text); background: var(--canvas); }
.project {
color: var(--text);
background: var(--surface);
border: 1px solid rgb(203 213 225);
border-inline-start: 0.35rem solid var(--accent);
padding: 1rem;
}
.hero {
color: white;
background-color: rgb(15 23 42);
background-image:
linear-gradient(120deg, rgb(15 23 42 / 96%), rgb(30 58 138 / 78%)),
url("images/workspace.jpg");
background-position: center;
background-size: cover;
padding: 4rem 1rem;
}
a { color: var(--accent); text-decoration-thickness: 0.1em; text-underline-offset: 0.2em; }
a:hover { text-decoration-thickness: 0.2em; }
a:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
Recap and exit questions
Think of color as layered paint: readability comes from the final foreground/background combination, not from either swatch considered alone. Modern functions use space-separated channels with slash alpha. Background images are decorative layers, while a background color beneath them provides resilience.
- Why is alpha risky for body text?
- In what order are comma-separated backgrounds painted?
- Why keep a background color beneath an image?
- What contrast ratios apply to normal and large text?
- Why should a project screenshot usually be an
<img>, not a background?
