demo · v138

Scrollytelling header

The header shrinks and tints between 40px and 160px of scroll — not a binary switch and not the whole document, but a precisely-bounded interpolation. clamp() wraps progress() so the scroll offset custom property yields a stable 0…1 ratio for every visual property.

checking progress() support…

Long-form report — section heading

Scroll down. The header above starts at 1.4rem and shrinks to 0.8rem over the 40–160px scroll window, the colour deepens, and the progress bar widens. JavaScript writes --scroll-y; CSS computes the interpolation with clamp(0, progress(var(--scroll-y), 40px, 160px), 1).

Before progress(), this kind of bounded mapping required a JS listener: read scrollTop, clamp, divide, set custom property. Doable but bound to a frame each time, and prone to jank when the listener falls behind the compositor.

With progress(), the formula lives in the stylesheet and the property update is hoisted into the compositor — no main-thread cost per frame.

This isn’t scroll-driven animation (CSS animation timeline). It’s simpler: a one-shot mapping you can use anywhere a length, angle, percentage, or number is accepted.

The values can come from anywhere — viewport width, container width, a custom property, even another progress(). Composability turns it into a real arithmetic primitive.

(end — scroll back up to watch the interpolation reverse exactly).

0%

the formula

.header {
  --scroll-y: 0px;
  --p: clamp(0, progress(var(--scroll-y), 40px, 160px), 1);
  font-size: calc(1.4rem - var(--p) * 0.6rem);
  background-color: color-mix(
    in srgb,
    var(--bg-paper),
    var(--accent-blue) calc(var(--p) * 28%)
  );
  color: color-mix(
    in srgb,
    var(--text-black),
    var(--accent-blue) calc(var(--p) * 35%)
  );
}
.progress-bar { width: calc(var(--p) * 100%); }

see also