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

# Gradients

Linear and radial color gradients; the rule for resolving stop positions, calculating intermediate colors, and producing a line with a hard transition.

Background layers got built with images coming from a file. A layer
can also get produced without a file: with calculated transitions
between colors.

**A gradient** is not a color, it is an **image**. This distinction
has consequences: it cannot get written into `background-color`, it
gets written into `background-image`; it has no intrinsic size and
gets sized by the area it gets placed in; `background-size`,
`background-position`, and `background-repeat` apply to it too.

## Color Stops

A gradient consists of **color stops**. Each stop carries a color and
an optional position. When positions do not get written, they get
filled in by a rule: the first stop lands at 0%, the last at 100%,
and the ones in between get distributed **at equal intervals**.

Every point between two stops is a linear mix of the two colors.

```js
// gradient.mjs — resolving color stops and calculating intermediate colors
const hexToRgb = (h) => ({ r: parseInt(h.slice(1,3),16), g: parseInt(h.slice(3,5),16), b: parseInt(h.slice(5,7),16) });
const hex = ({r,g,b}) => "#" + [r,g,b].map(v => Math.round(v).toString(16).padStart(2,"0")).join("");

// stops with no position get distributed evenly between neighboring known points
function resolveStops(stops) {
  const resolved = stops.map((s) => ({ ...s }));
  if (resolved[0].position == null) resolved[0].position = 0;
  if (resolved.at(-1).position == null) resolved.at(-1).position = 100;
  let i = 0;
  while (i < resolved.length) {
    if (resolved[i].position != null) { i++; continue; }
    let j = i;
    while (resolved[j].position == null) j++;
    const start = resolved[i - 1].position, end = resolved[j].position;
    const step = (end - start) / (j - i + 1);
    for (let t = i; t < j; t++) resolved[t].position = +(start + step * (t - i + 1)).toFixed(2);
    i = j;
  }
  // stop positions cannot decrease: raise anything that falls below the previous value
  for (let t = 1; t < resolved.length; t++) if (resolved[t].position < resolved[t-1].position) resolved[t].position = resolved[t-1].position;
  return resolved;
}

function midColor(stops, percent) {
  const resolved = resolveStops(stops);
  if (percent <= resolved[0].position) return resolved[0].color;
  if (percent >= resolved.at(-1).position) return resolved.at(-1).color;
  for (let i = 1; i < resolved.length; i++) {
    if (percent <= resolved[i].position) {
      const a = resolved[i-1], b = resolved[i];
      const t = b.position === a.position ? 0 : (percent - a.position) / (b.position - a.position);
      const ca = hexToRgb(a.color), cb = hexToRgb(b.color);
      return hex({ r: ca.r + (cb.r-ca.r)*t, g: ca.g + (cb.g-ca.g)*t, b: ca.b + (cb.b-ca.b)*t });
    }
  }
}

const A = [{ color: "#143a52" }, { color: "#8a9da9" }, { color: "#ffffff" }];
const B = [{ color: "#143a52", position: 0 }, { color: "#8a9da9", position: 20 }, { color: "#ffffff", position: 100 }];
const C = [{ color: "#143a52", position: 0 }, { color: "#143a52", position: 50 }, { color: "#ffffff", position: 50 }, { color: "#ffffff", position: 100 }];

for (const [label, stops] of [["equal distribution", A], ["manually positioned", B], ["hard transition (line)", C]]) {
  console.log(`--- ${label} ---`);
  console.log("resolved stops:", resolveStops(stops).map((x) => `${x.color}@${x.position}%`).join(" "));
  const samples = [0, 10, 20, 25, 49, 50, 51, 75, 100];
  console.log(samples.map((p) => `${String(p).padStart(3)}%:${midColor(stops, p)}`).join("  "));
  console.log("");
}

console.log("--- angle direction (linear-gradient) ---");
for (const [angle, direction] of [[0,"bottom to top"],[90,"left to right"],[180,"top to bottom"],[270,"right to left"]]) {
  const rad = (angle - 90) * Math.PI / 180;
  console.log(`${String(angle).padStart(3)}deg -> ${direction.padEnd(18)} unit vector (${Math.cos(rad).toFixed(3)}, ${(-Math.sin(rad)).toFixed(3)})`);
}
```

```
--- equal distribution ---
resolved stops: #143a52@0% #8a9da9@50% #ffffff@100%
  0%:#143a52   10%:#2c4e63   20%:#436275   25%:#4f6c7e   49%:#889ba7   50%:#8a9da9   51%:#8c9fab   75%:#c5ced4  100%:#ffffff

--- manually positioned ---
resolved stops: #143a52@0% #8a9da9@20% #ffffff@100%
  0%:#143a52   10%:#4f6c7e   20%:#8a9da9   25%:#91a3ae   49%:#b4c1c8   50%:#b6c2c9   51%:#b7c3ca   75%:#dae0e4  100%:#ffffff

--- hard transition (line) ---
resolved stops: #143a52@0% #143a52@50% #ffffff@50% #ffffff@100%
  0%:#143a52   10%:#143a52   20%:#143a52   25%:#143a52   49%:#143a52   50%:#143a52   51%:#ffffff   75%:#ffffff  100%:#ffffff

--- angle direction (linear-gradient) ---
  0deg -> bottom to top      unit vector (0.000, 1.000)
 90deg -> left to right      unit vector (1.000, 0.000)
180deg -> top to bottom      unit vector (0.000, -1.000)
270deg -> right to left      unit vector (-1.000, -0.000)
```

The first block shows the equal distribution: three stops got
written, and the middle one landed at 50%. The color at the `10%`
point, `#2c4e63`, is a one-fifth mix of the first two stops.

The second block uses the same three colors with different positions.
Once the middle stop gets pulled to 20%, the transition's **speed**
changes: the color lightens quickly in the first fifth, then slows in
the remainder. At the `10%` point, the color that was `#2c4e63` in
the first block is `#4f6c7e` here — much lighter.

## Producing a Line With a Hard Transition

The third block shows a pattern that looks like a trick but is a
direct consequence of the definition. When two stops get written at
**the same position**, no distance remains between them to mix over,
and the gradient turns into an edge: still dark at 49%, fully white
at 51%.

This is the way to produce lines, stripes, and checkerboard patterns
with gradients:

```css
.measurement-table {
  background-image: linear-gradient(
    to right,
    var(--line) 0 1px,
    transparent 1px 100%
  );
  background-repeat: repeat-x;
  background-size: 120px 100%;
}
```

Writing two positions on one stop (`0 1px`) declares that the color
stays constant across that range; it is a shorthand for writing two
stops.

## Three Gradient Types

**`linear-gradient()`** — colors change along a line. Direction gets
given with an angle or a keyword. The output's last block gives the
angle measurement: `0deg` points upward, and the angle increases
clockwise. The writing `to top` is the same as `0deg`, `to right` the
same as `90deg`. Positive y in the vectors points upward.

**`radial-gradient()`** — colors change outward from a center. Shape
(`circle` or `ellipse`), size (like `closest-side`,
`farthest-corner`), and the center can also be given.

**`conic-gradient()`** — colors change by angle around a center. This
is the type that pie charts and color wheels can get written with.

All three have `repeating-` prefixed forms; they run the stop
sequence to its end and restart it from the beginning.

## Banding and the Mixing Space

In a gradient between two close colors over a large area, color steps
becoming visible is common. In eight-bit channels, intermediate
values get rounded, and over a large area, neighboring regions that
round to the same value look like a band.

Which color space the mix gets done in can get declared:

```css
.surface {
  background-image: linear-gradient(in oklab, #143a52, #8a5b00);
}
```

When the space does not get declared, the mix happens in the sRGB
space. A mix done in a perceptually uniform space reduces the
washing-out of the in-between tones between two colors — a visible
difference especially in transitions between complementary colors.

Whether this writing is recognized can get tested, and if it is not,
the declaration before it stays in place:

```css
.surface { background-image: linear-gradient(#143a52, #8a5b00); }

@supports (background-image: linear-gradient(in oklab, red, blue)) {
  .surface { background-image: linear-gradient(in oklab, #143a52, #8a5b00); }
}
```

## Gradients Are an Image

The distinction from the start of this lesson has practical
consequences.

A gradient is a `background-image` layer; multiple gradients can get
stacked with a comma, and transparent regions show the layer below.

A gradient has no intrinsic size; when `background-size` does not get
given, it fills the area it gets placed in. When it does get given,
it can get tiled with `background-repeat` — the line example above
rests on this.

A gradient downloads no file. The same visual effect could also get
achieved with an image file; a gradient produces it without an extra
request, and rescales itself whenever the area changes.

```css
/* station.css — step 22: gradients */
.masthead {
  background-image:
    linear-gradient(to bottom, hsl(var(--hue) 61% 96%), hsl(var(--hue) 18% 97%));
}

.measurement-chart {
  background-image:
    linear-gradient(to top, var(--line) 0 1px, transparent 1px 100%);
  background-size: 100% 40px;
}

.loading-bar {
  background-image: linear-gradient(
    to right,
    var(--brand-dark) 0 var(--percent, 0%),
    var(--line) var(--percent, 0%) 100%
  );
}
```

The `.measurement-chart` class places a horizontal grid line every 40
units behind the measurement chart. The lines do not exist in the
document and should not: a grid line carries no information, it is a
visual aid that helps reading.

The `.loading-bar` class uses a custom property as the stop position.
When the `--percent` value changes, the bar's filled portion changes;
the gradient itself does not get rewritten. If the value comes from
the document, the fill ratio also has to get declared accessibly —
with a `progress` element, say, or the appropriate ARIA attributes.
The visual bar alone does not correspond to a state.

## Summary

- A gradient is not a color, it is a background image; it gets
  written into `background-image`, has no intrinsic size, and gets
  sized by the area it gets placed in.
- Unwritten stop positions get filled in: the first stop at 0%, the
  last at 100%, and the ones in between get distributed at equal
  intervals between neighboring known points.
- Changing the stop positions changes the gradient's speed; when two
  stops get written at the same position, no distance remains to mix
  over, and a sharp edge forms.
- Writing two positions on one stop declares that the color stays
  constant across the range; line and stripe patterns get produced
  with this writing, without downloading a file.
- The color space the mix happens in can get declared; when it does
  not get declared, sRGB gets used, and the in-between tones between
  distant colors can wash out.

## Next Step

The boxes' inside and back are complete. Their edges are missing: the
border's drawing style, rounding the corners, and shadows falling
outside the box have not been taken up yet. The next lesson combines
these three topics and shows how the corner radius gets calculated
with the border thickness.
