021: Colors and Backgrounds
Learning outcomes
By the end, you can choose readable foreground/background pairs; write hex, named, rgb(), and hsl() colors; add alpha transparently; distinguish color from background properties; create useful gradients and images with fallbacks; and test contrast and decorative backgrounds.
Prerequisites and retrieval
Open the portfolio stylesheet. Retrieve which declaration wins between .project { color: blue; } and article.project { color: green; }. Identify inherited text color. You will now build a small palette without increasing selector specificity.
Terminology
- Color space/model: A method 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 via the color property. — Source: MDN: values
- Background layer: One comma-separated background painting layer (image/gradient) above background-color. — Source: MDN: Using multiple backgrounds
- Alpha: An opacity channel from 0 (fully transparent) through 1 (opaque). — Source: MDN: values
- Gradient: A generated image transitioning between two or more color stops. — Source: MDN: Applying color
- Contrast ratio: The relative-luminance comparison between foreground and background, (L1+0.05)/(L2+0.05). — Source: WCAG 2.2: Contrast Minimum
- Fallback: A simpler earlier declaration retained when a later one 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
Each element paints layers. A background color sits underneath any background image; content sits above backgrounds. color supplies the current foreground color and is inherited by text and often by decorations. Transparent paint reveals whatever is behind it, so contrast must be checked against the resulting composite, not an imagined white canvas.
Common stable formats are:
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 */
Use the format your team can maintain. Hex is compact; rgb() is explicit and supports readable alpha; hsl() can make related manual variations intuitive. A format does not guarantee accessible contrast.
Beginner example: a coherent portfolio palette
Start with a neutral canvas, dark text, one accent, and visible interaction colors:
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;
}
Observe inheritance: the header heading receives white from header; its paragraph receives the more subdued blue-white directly. currentColor means the computed color value, keeping the hover underline coordinated. Alpha is appropriate for a nonessential underline, but avoid faint transparent body text.
Test zoom and keyboard focus. Switch the body background to dark temporarily. Any transparent backgrounds and borders reveal their dependency on surrounding paint.
Intermediate example: background layers and gradients
Create a hero that remains usable if its decorative image is unavailable:
<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;
}
Background images are comma-separated layers: the first is painted closest to the viewer. The dark gradient protects text contrast over a variable photograph. The color remains a fallback while the file downloads or if it fails. Background images convey no accessible name, so keep them decorative; meaningful portfolio screenshots belong in <img> with suitable alt text.
A generated gradient can create a section accent without network cost:
.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;
}
Do not put essential information in decorative paint. The heading remains meaningful if all backgrounds are disabled.
Optional advanced example: maintainable custom properties
Custom properties are stable and can name repeated 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 roles rather than visual values: --color-accent can change without becoming a misleading --blue. Keep the first palette simple; custom properties do not replace contrast testing.
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.
In DevTools, inspect the element and click a color swatch. Use the picker to inspect rendered colors and its contrast information where supported. Toggle individual background layers. The Network panel reveals failed image requests; Computed shows final color and background-color. Simulate forced colors if your browser supports it and confirm controls remain identifiable.
Accessibility and performance
WCAG 2.2 contrast minimum is 4.5:1 for normal text and 3:1 for large text (at least 18pt, or 14pt bold, in WCAG's definitions). User-interface components and meaningful graphical objects generally need 3:1 against adjacent colors. Logos and incidental/inactive content have different exceptions, but do not use exceptions as a design target.
Do not rely on color alone for errors, selection, or required fields. Preserve underlines or another non-color cue for inline links. Gradient/photo combinations require testing at several crop positions and widths.
Compress photographs, provide dimensions for content images, and choose appropriate formats. A CSS gradient is cheap compared with a large bitmap. Avoid enormous off-screen backgrounds and repeated downloads. Decorative images can be CSS backgrounds; meaningful images should use responsive HTML images, which provide semantics and stronger loading controls.
Deep dive: color formats and alpha
CSS accepts several color syntaxes. Choose a project convention that remains readable to the team.
.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 syntax can include alpha:
.overlay {
background: rgb(15 23 42 / 0.72);
}
Prefer alpha on the color itself when you only want that paint to be translucent. opacity affects the entire element, including 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 can contain multiple images or gradients. The first listed layer is painted 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;
}
Think of this as transparent sheets stacked over the element's background color. The gradient supplies readable contrast while the photo supplies context.
Background shorthand is compact but easy to misread. Learn the longhands first:
background-colorbackground-imagebackground-repeatbackground-positionbackground-sizebackground-attachmentbackground-originbackground-clip
Then use shorthand only when the team can still understand the intent.
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);
}
The custom properties are named for roles, not exact hues. A future theme can change the palette without renaming --blue-500 to a non-blue color.
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 text remains actual HTML. The image is decorative/background content; if the image itself conveys essential information, use <img> with appropriate alternative text instead.
Deep dive: gradients are generated images
Useful gradient families:
.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 produce charts or decorative effects, but do not encode data using color alone. Real data visualizations need labels and accessible textual equivalents.
Color-debugging checklist
When a palette looks wrong, check:
- 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?
Treat color as part of the interaction system, not merely decoration.
Background attachment: use with restraint
.hero {
background-image: url("/images/landscape.jpg");
background-attachment: scroll;
}
scroll is the normal behavior. fixed can create a viewport-anchored/parallax-like effect:
.hero {
background-attachment: fixed;
}
Do not assume fixed behaves identically across mobile browsers, and do not make a visual effect essential to understanding the page. Large fixed backgrounds can also be expensive to paint.
Background-position and focal points
.hero {
background-position: 70% 35%;
}
This is useful when the subject is not centered. Re-test at multiple aspect ratios: a crop that protects a face on desktop may cut it off on narrow screens.
Tiered exercises
Checkpoint: test the palette as a system
Create a small matrix containing body text, muted text, links, focus, card borders, and text on the hero. Check each foreground against the background on which it actually appears. Then test visited, hover, focus, and disabled states if present. A palette is complete only when interaction states remain distinguishable and readable, not when five swatches look harmonious in isolation.
Temporarily disable the hero image and then the gradient. The color fallback should keep content understandable during loading and failure. Finally, view the page in grayscale or with a color-vision simulation: links, errors, and selection should retain a cue other than hue.
Record palette decisions by role and state: canvas, surface, primary text, muted text, accent, visited link, focus, success, and error. Not every project needs every role, but naming the ones used exposes inconsistent duplicates. Verify that muted text is still readable; “muted” should mean less emphasis, not 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
Color is layered paint, and readability depends on the final foreground/background pair. Modern functions use space-separated channels and slash alpha. Background images are decorative layers; a background color supplies 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?
