DEV Community

JKC
JKC

Posted on

Stop Doing Algebra in clamp(). CSS progress() Is Here

TL;DR: progress() turns any value into a number between 0 and 1. That makes fluid type and spacing readable, and since September 2026 it works in every major browser.

The line nobody wants to touch

Every codebase has one:

h1 {
  /* 1rem at 360px, 2rem at 1280px. Trust me. */
  font-size: clamp(1rem, 0.6087rem + 1.7391vw, 2rem);
}
Enter fullscreen mode Exit fullscreen mode

It works. It also looks like a lottery ticket.

Where did 0.6087rem come from? A fluid type calculator, in a tab that closed years ago.

Now your designer moves the breakpoint to 1440px. Time to redo the algebra.

Meet progress()

progress() answers one question: how far along is this value, between a start and an end?

progress(<value>, <start>, <end>)
Enter fullscreen mode Exit fullscreen mode

Under the hood it is (value - start) / (end - start), clamped to the range 0 to 1.

.demo {
  opacity: progress(5, 0, 10); /* 0.5 */
  scale: progress(15, 0, 10); /* 1, not 1.5. It clamps. */
}
Enter fullscreen mode Exit fullscreen mode

Think of it as a progress bar for any CSS value. The fun part: the arguments can be lengths.

progress(100vw, 360px, 1280px) is 0 on a 360px screen, 1 at 1280px and up, and slides smoothly in between.

The same heading, minus the homework

h1 {
  /* 1rem, plus up to 1rem more between 360px and 1280px */
  font-size: calc(1rem + 1rem * progress(100vw, 360px, 1280px));
}
Enter fullscreen mode Exit fullscreen mode

The breakpoints are in the code. The sizes are in the code. No slope, no intercept, and no clamp(), because progress() already clamps.

Designer moves the breakpoint to 1440px? Change 1280px to 1440px. Done.

I tested both versions in Chrome 154 at viewport widths from 320px to 1600px. They match, except the hand-rounded clamp() gives 31.9997px at 1280px, while progress() gives a clean 32px. Small win, but I'll take it.

Need several things to grow together? Name the ratio once:

:root {
  --fluid: progress(100vw, 360px, 1280px);
}

h1 {
  font-size: calc(1rem + 1rem * var(--fluid));
}

.section {
  padding-block: calc(2rem + 4rem * var(--fluid));
}
Enter fullscreen mode Exit fullscreen mode

Keep a rem in the base, like the 1rem + above, so text still respects the reader's font size setting.

Cards that size themselves by their container

The viewport is not always the right ruler. A card in a sidebar should not act like it owns the whole screen. Container query units work as inputs too:

<div class="card-slot">
  <article class="card">
    <h2 class="card__title">Release notes</h2>
    <p>Everything that shipped this week.</p>
  </article>
</div>
Enter fullscreen mode Exit fullscreen mode
.card-slot {
  container-type: inline-size;
}

.card {
  /* 0 when the slot is 280px or narrower, 1 at 560px or wider */
  --size: progress(100cqi, 280px, 560px);

  padding: calc(0.75rem + 0.75rem * var(--size));
  border-radius: calc(6px + 6px * var(--size));
}

.card__title {
  font-size: calc(1.125rem + 0.375rem * var(--size));
}
Enter fullscreen mode Exit fullscreen mode

What Chrome 154 actually computed:

Slot width Padding Radius Title
200px 12px 6px 18px
420px 18px 9px 21px
900px 24px 12px 24px

Notice the wrapper. Container query units resolve against the nearest ancestor container, so the card can't measure itself. Somebody else has to hold the tape measure.

Can I use it?

Yes:

  • Chrome and Edge 138
  • Safari 26
  • Firefox 155, which shipped in September 2026 and made progress() Baseline Newly available

Older browsers won't have it, so keep a fallback (gotcha 5 shows the right way).

Gotchas

1. It returns a number, not a length. progress() gives you something like 0.4. Multiply it by a unit inside calc().

2. Don't mix types. progress(3em, 0px, 100px) is fine, since both are lengths. progress(3s, 0px, 100px) is invalid, and the browser throws out the whole declaration. Seconds and pixels are not friends.

3. Same start and end? No explosion. progress(5, 5, 5) is 0, not a divide-by-zero meltdown. The spec says so.

4. no-clamp isn't in Chrome yet. progress(no-clamp 15, 0, 10) should give 1.5. Firefox 155 and Safari 27 support it, Chrome 154 doesn't, so skip it in production for now.

5. The custom property fallback trap. This fallback works, because older browsers drop the second line while parsing:

h1 {
  font-size: 1.5rem;
  font-size: calc(1rem + 1rem * progress(100vw, 360px, 1280px));
}
Enter fullscreen mode Exit fullscreen mode

This one quietly doesn't:

h1 {
  font-size: 1.5rem;
  font-size: calc(1rem + 1rem * var(--fluid));
}
Enter fullscreen mode Exit fullscreen mode

With var(), the browser accepts the line at parse time, so 1.5rem has already lost. When --fluid later turns out to be gibberish to an old browser, font-size falls back to its inherited or initial value, not to your fallback. I checked this by swapping progress() for a made-up function: the plain fallback held at 20px, while the var() version shrank to the parent's 10px.

Fix it with a feature query:

.card {
  padding: 1rem;
}

@supports (padding: calc(1px * progress(1, 0, 2))) {
  .card {
    --size: progress(100cqi, 280px, 560px);
    padding: calc(0.75rem + 0.75rem * var(--size));
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrap-up

progress() lets your CSS say what you meant: this value, between these two sizes. Go find the clamp() line in your codebase that nobody dares to touch. Today's the day.

Sources

This article was written with the help of AI and checked against the sources above.

Top comments (0)