---
title: Backgrounds
source: 'https://academia.sh/en/courses/css-fundamentals/backgrounds'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:17+00:00'
license: 'CC BY-SA 4.0'
---

# Backgrounds

Background layers, the calculation behind percentage positioning, repeat modes, and the numeric counterpart of cover and contain scaling.

The text decisions are complete. The back of the boxes has not been
looked at yet. The Box Model lesson established that the background
extends to the border box and covers the padding; what the background
itself can be was not said.

A background is not just a color. It can be a **layer stack** made of
positionable, repeatable, scalable images. This lesson takes up those
layers' rules and calculations.

## A Background Is a Layer Stack

`background` is a shorthand and sets eight longhands: `-color`,
`-image`, `-position`, `-size`, `-repeat`, `-origin`, `-clip`,
`-attachment`.

`background-image` can take multiple comma-separated values. Each
value establishes a **layer**, and the layers get ordered **left to
right, top to bottom**: the first member of the list gets drawn on
top.

`background-color` is not a layer; it always gets drawn at the very
bottom, as a single color. This is why it cannot get multiplied with
a comma.

The other longhands also get multiplied with a comma and get matched
to the layers in order. The number of layers is determined by the
length of the `background-image` list; lists that fall short get
repeated from the start.

## A Percentage Position Is an Alignment

When `background-position` takes a percentage, that percentage is not
a distance, it is an **alignment**: the image's $p$ percent point gets
matched to the area's $p$ percent point. The result:

$$x = (A_{\text{width}} - I_{\text{width}}) \cdot p$$

```js
// background.mjs — background-position percentage and repeat math
// percentage positioning: the image's p% point lands on the area's p% point
function position(areaWidth, imageWidth, value) {
  if (typeof value === "number") return value;                 // px
  const p = parseFloat(value) / 100;
  return (areaWidth - imageWidth) * p;
}

const AREA = { width: 400, height: 200 };
const IMAGE = { width: 120, height: 60 };

console.log(`area ${AREA.width}x${AREA.height}, image ${IMAGE.width}x${IMAGE.height}`);
for (const [x, y] of [["0%","0%"], ["50%","50%"], ["100%","100%"], ["25%","75%"], [16, 16]]) {
  const left = position(AREA.width, IMAGE.width, x);
  const top = position(AREA.height, IMAGE.height, y);
  console.log(`position: ${String(x).padEnd(5)} ${String(y).padEnd(5)} -> top-left (${left}, ${top})`);
}

console.log("\n--- repeat: tile starting points ---");
function tiles(area, image, start, repeatMode) {
  if (repeatMode === "no-repeat") return [start];
  const points = [];
  let x = start;
  while (x > -image) x -= image;   // rewind leftward
  x += image;
  while (x < area) { points.push(x); x += image; }
  return points;
}
for (const mode of ["repeat", "no-repeat"]) {
  const points = tiles(AREA.width, IMAGE.width, position(AREA.width, IMAGE.width, "50%"), mode);
  console.log(`${mode.padEnd(10)} -> ${points.length} tile${points.length === 1 ? "" : "s"}, x = ${points.join(", ")}`);
}

console.log("\n--- repeat: leftover horizontal space ---");
for (const areaWidth of [400, 360, 480]) {
  const whole = Math.floor(areaWidth / IMAGE.width);
  const remainder = areaWidth - whole * IMAGE.width;
  const roundScale = areaWidth / Math.round(areaWidth / IMAGE.width);
  console.log(`area=${areaWidth}: repeat -> ${whole} whole + ${remainder}px cropped | round -> ${Math.round(areaWidth / IMAGE.width)} tiles, each ${roundScale.toFixed(2)}px | space -> ${whole} tiles, ${whole > 1 ? (remainder / (whole - 1)).toFixed(2) : 0}px between`);
}

console.log("\n--- background-size: cover and contain ---");
function scale({ areaWidth, areaHeight, imageWidth, imageHeight }, mode) {
  const scaleX = areaWidth / imageWidth, scaleY = areaHeight / imageHeight;
  const factor = mode === "cover" ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
  return { width: +(imageWidth * factor).toFixed(1), height: +(imageHeight * factor).toFixed(1), factor: +factor.toFixed(3) };
}
const dims = { areaWidth: 400, areaHeight: 200, imageWidth: 800, imageHeight: 600 };
for (const mode of ["cover", "contain"]) {
  const result = scale(dims, mode);
  console.log(`${mode.padEnd(8)} factor=${result.factor} -> ${result.width}x${result.height}  (area ${dims.areaWidth}x${dims.areaHeight})`);
}
```

```
area 400x200, image 120x60
position: 0%    0%    -> top-left (0, 0)
position: 50%   50%   -> top-left (140, 70)
position: 100%  100%  -> top-left (280, 140)
position: 25%   75%   -> top-left (70, 105)
position: 16    16    -> top-left (16, 16)

--- repeat: tile starting points ---
repeat     -> 5 tiles, x = -100, 20, 140, 260, 380
no-repeat  -> 1 tile, x = 140

--- repeat: leftover horizontal space ---
area=400: repeat -> 3 whole + 40px cropped | round -> 3 tiles, each 133.33px | space -> 3 tiles, 20.00px between
area=360: repeat -> 3 whole + 0px cropped | round -> 3 tiles, each 120.00px | space -> 3 tiles, 0.00px between
area=480: repeat -> 4 whole + 0px cropped | round -> 4 tiles, each 120.00px | space -> 4 tiles, 0.00px between

--- background-size: cover and contain ---
cover    factor=0.5 -> 400x300  (area 400x200)
contain  factor=0.333 -> 266.7x200  (area 400x200)
```

The first block's `50% 50%` value giving 140 and 70 is the
calculation's direct result: half the difference between the area and
the image. The result is the image getting centered. `100% 100%`
pushes it to the bottom-right corner, `0% 0%` to the top-left.

The difference with pixel values shows up in the last row: `16px
16px` is always 16 units from the edge, independent of the area's and
image's dimensions. A percentage declares an alignment, a length
declares a distance.

`background-position` also accepts a four-value writing, which
selects which edge to measure from: the writing `right 16px bottom
16px` places it 16 units in from the bottom-right corner.

## Repeat Modes

The second block shows that in `repeat` mode, tiling starts
**outside** the visible area: the first tile sits at $-100$.
Repetition is defined as extending the positioned tile to infinity in
both directions; whatever falls outside the visible area gets
cropped.

The third block gives the difference between the three modes in
numbers. A 120-unit image in a 400-unit area:

- `repeat` — 3 whole tiles fit, a 40-unit remainder appears cropped.
- `round` — the tile count gets rounded and the image gets
  **scaled**: 3 tiles, each 133.33 units. No cropping remains, but
  the image's aspect ratio breaks.
- `space` — the tile count gets rounded down and the leftover space
  gets **distributed between the gaps**: 3 tiles, 20 units apart
  each. The scale is preserved, a gap opens up.

When the area is an exact multiple of the image (the 360 and 480
rows), all three modes give the same result.

`repeat-x` and `repeat-y` repeat on only one axis; `no-repeat` does
not repeat at all.

## cover and contain

The last block compares the two scaling keywords. An 800×600 image in
a 400×200 area:

`cover` — picks the **larger** of the two axes' scale ratios. The
result is 400×300: the image fills the area completely, and the
overflowing part gets cropped.

`contain` — picks the **smaller** one. The result is 266.7×200: the
whole image is visible, and part of the area stays empty.

The decision gets made by this question: is cropping acceptable, or
is leftover space? `cover` gets written for a decorative image,
`contain` for an image that needs to be seen in full.

When `cover` gets used, where the cropping happens gets directed with
`background-position`; if the image's important part is near an
edge, centered cropping cuts it off.

## The Background's Boundaries

Three properties determine which region the background relates to,
and the region names from the Box Model lesson appear here as values.

`background-origin` — chooses **which box** positioning gets measured
relative to. Its initial value is `padding-box`.

`background-clip` — chooses **how far** the background gets drawn.
Its initial value is `border-box`; this is why the background shows
through the gaps of a dashed border. When `content-box` gets written,
the background does not enter the padding.

`background-attachment` — chooses whether the background scrolls
together with the box (`scroll`, `local`) or gets fixed to the
viewport (`fixed`).

## Image Backgrounds and Accessibility

A background image does not exist in the document: it has no
alternative text, does not get declared to assistive technology, and
does not get read as content by a search engine. This is the same
boundary as the one for content produced with pseudo-elements.

The consequence is a rule: **no image that carries meaning gets
placed as a background.** A location map has to stand in the document
as an `img` element with alternative text. Backgrounds are for
decorative textures and surfaces that text gets written over.

The legibility of text written over a base image also needs to get
secured. If the image does not download or its download is delayed,
the text faces the background **color**; the contrast between that
color and the text color has to pass the criterion calculated in the
previous lesson.

```css
/* station.css — step 21: background layers */
.masthead {
  background-color: var(--surface);
  background-image: url("/images/topography.svg");
  background-position: right -40px top 50%;
  background-repeat: no-repeat;
  background-size: 320px auto;
}

.measurement-table tbody tr:nth-child(odd) {
  background-color: hsl(var(--hue) 18% 97%);
  background-clip: padding-box;
}

.warning-box {
  background-color: hsl(0 66% 97%);
  background-image: url("/images/warning.svg");
  background-position: 12px center;
  background-repeat: no-repeat;
  background-size: 20px 20px;
  padding-inline-start: 44px;
}
```

The pattern in the masthead is decorative and gets written together
with `background-color`: if the image does not download, the surface
color remains, and the heading text's contrast stays intact.

The icon in the `.warning-box` class is decorative too; the warning
itself is written in the text inside the box. The icon is only a
visual marker for the text, and `padding-inline-start` makes room for
it — the icon not overlapping the text depends on this declaration.

## Summary

- `background-image` builds a layer stack by getting multiplied with
  a comma; the first member of the list gets drawn on top, and
  `background-color` always stays at the bottom, as a single color.
- A percentage background position is an alignment, not a distance:
  the image's $p$ percent point gets matched to the area's $p$
  percent point; this is why `50%` centers.
- Repeat modes handle the leftover space differently: `repeat` crops
  it, `round` removes the crop by scaling the image, `space`
  distributes the leftover between the tiles.
- `cover` picks the larger of the two scale ratios, fills the area,
  and crops the overflow; `contain` picks the smaller one, shows the
  whole image, and leaves empty space.
- A background image does not exist in the document and does not get
  declared to assistive technology; images that carry meaning belong
  in the markup, backgrounds get written only as a decorative layer.

## Next Step

Background layers got built with images coming from a file. A layer
can also get produced without a file: with calculated transitions
between colors. The next lesson takes up linear and radial gradients,
the calculation behind color stops, and why gradients count as a
background image, not an image.
