Tech · · 5 min read
CSS progress() is Baseline. It is not what the name suggests
progress() returns how far one value sits between two others, as a number from 0 to 1. What it computes, the fluid typography it simplifies, and what it doesn't do.
progress() takes three values and returns how far the first sits between the other two, as a unitless number clamped to 0–1:
opacity: progress(5, 0, 10); /* 0.5 */
opacity: progress(50px, 0px, 100px); /* 0.5 */It has nothing to do with scroll progress, animation progress or <progress>. It's arithmetic: (value − start) / (end − start), with the result clamped. The CSS Values Level 5 draft describes it as "essentially syntactic sugar for a particular pattern of calc() notations".
That sugar turns out to matter, because the pattern it replaces is the one everybody gets wrong: fluid typography. progress() became Baseline newly available on 1 September 2026 when Firefox 155 shipped it.
The syntax
progress(no-clamp? <calc-sum>, <calc-sum>, <calc-sum>)The three arguments are the progress value, the start value and the end value, in that order. no-clamp is a bare keyword before the first argument, with no comma after it.
| Expression | Result |
|---|---|
progress(5, 0, 10) | 0.5 |
progress(15, 0, 10) | 1 — clamped |
progress(no-clamp 15, 0, 10) | 1.5 |
progress(no-clamp -5, 0, 10) | -0.5 |
The return value is always a plain <number>, whatever units go in. To get a length back out, multiply it:
width: calc(progress(100vw, 320px, 1200px) * 600px);The arguments must share a type
The three arguments can be numbers, dimensions or percentages, and they can use different units — but they must resolve to the same type, or the function is invalid and the declaration is dropped.
/* Valid: all lengths. 3em resolves to 48px at a 16px font size,
which is 48% of the way from 0px to 100px, so this returns 0.48 */
progress(3em, 0px, 100px)
/* Invalid: a time can't be measured against lengths */
progress(3s, 0px, 100px)
/* Invalid: a length can't be measured against unitless numbers */
progress(3em, 0, 100)That last one is the mistake worth remembering. Mixing a length with bare numbers looks harmless and silently kills the declaration.
Fluid typography, without the magic numbers
The standard fluid-type recipe asks you to work out a slope and an intercept by hand, then wrap them in clamp():
/* 1rem at a 320px viewport, 2rem at 1200px */
font-size: clamp(1rem, 0.636rem + 1.818vw, 2rem);Nobody reads 0.636rem + 1.818vw and knows what it does. You have to trust the calculator you generated it with, and you have to regenerate it whenever a designer changes a breakpoint.
With progress(), the same curve states its own intent:
font-size: calc(1rem + progress(100vw, 320px, 1200px) * 1rem);Read it back: start at 1rem, and add up to 1rem more as the viewport travels from 320px to 1200px. The two forms produce the same value — at a 700px viewport both give about 1.43rem, assuming a 16px root font size — but only one of them survives a code review.
The clamping is free. Below 320px the function returns 0, above 1200px it returns 1, so the size holds at 1rem and 2rem without a separate clamp().
Naming the ends
Custom properties make it reusable, which is where this starts to look like a type scale rather than a one-off:
:root {
--fluid-min: 320px;
--fluid-max: 1200px;
--fluid: progress(100vw, var(--fluid-min), var(--fluid-max));
}
h1 { font-size: calc(1.75rem + var(--fluid) * 1.75rem); }
p { font-size: calc(1rem + var(--fluid) * 0.125rem); }
.section { padding-block: calc(2rem + var(--fluid) * 4rem); }One ratio, computed once, reused across type and spacing. Every element ramps between the same two viewport widths, which is the thing hand-written vw formulas quietly fail to guarantee.
Other places it earns its keep
Driving a value from a container's own size. Because the arguments can be custom properties, anything you can set — from JavaScript, from a container query, from an inline style — can drive an interpolation:
.card {
--lift: progress(var(--depth), 0, 10);
box-shadow: 0 calc(var(--lift) * 24px) calc(var(--lift) * 48px) rgb(0 0 0 / 0.2);
}Interpolating colour channels. The MDN reference shows progress() used inside rgb() to ramp two channels together. It works, but color-mix() is usually the clearer tool for colour; reach for progress() when you need the ratio itself for something else too.
Extrapolating deliberately. no-clamp lets a value keep going past its endpoints — useful for a parallax-style offset that should overshoot, and dangerous everywhere else. Clamped is the default for a reason.
What progress() does not do
- It doesn't track scrolling. Scroll-linked effects are the job of scroll-driven animations and
animation-timeline.progress()has no timeline and no notion of time. - It doesn't animate. The value changes when its inputs change — a resize, a custom property update — and that's it. Nothing eases.
- It doesn't return a percentage. The output is a unitless number, so
width: progress(...)is invalid. Multiply by100%if a percentage is what you want. The CSS Working Group has an open issue about whether apercent-progress()notation is needed. - It doesn't replace
clamp().clamp()bounds a value you already have;progress()produces a ratio. They compose fine.
Browser support
Baseline newly available since 1 September 2026. At the time of writing:
| Browser | Version |
|---|---|
| Chrome and Edge | 138 |
| Chrome for Android | 138 |
| Firefox (desktop and Android) | 155 |
| Safari (macOS and iOS) | 26 |
Firefox was the last engine to ship it, which is why the Baseline date is so recent even though Chrome has had it since mid-2025. "Newly available" means all four engines support it now, not that every visitor's browser is up to date — an older Safari or Firefox will drop the declaration entirely, so keep a static fallback above it:
h1 {
font-size: 2.5rem; /* used if progress() isn't understood */
font-size: calc(1.75rem + progress(100vw, 320px, 1200px) * 1.75rem);
}FAQ
Is progress() related to scroll-driven animations?
No. Despite the name, it takes no timeline. Use animation-timeline: scroll() or view() for anything tied to scroll position.
What happens if the start and end values are the same?
With clamping on, the function returns 0. With no-clamp, the spec defines it as 0, −∞ or +∞ depending on whether the progress value is equal to, below or above the shared value.
Can I use percentages?
Yes, as long as all three arguments are percentages — progress(50%, 30%, 80%) is valid. You can't mix a percentage with a bare number.
Does it work in @keyframes?
Yes, it's an ordinary math function and valid anywhere calc() is. It won't create motion on its own; it only computes a value.
Sources
- CSS Values and Units Module Level 5, the progress() notation — CSS Working Group editor's draft
progress()on MDN- web-features:
progress-function— Baseline status and dates @mdn/browser-compat-data— per-browser version data
More to read
- Design
Design · · 6 min read
Staggered animations in plain CSS, now that sibling-index() is Baseline
sibling-index() and sibling-count() became Baseline newly available in August 2026. How to stagger a list without JavaScript, and what breaks if you're careless.
- Design
Design · · 5 min read
Auto-growing textareas, without the JavaScript: field-sizing is Baseline
One CSS declaration replaces the scrollHeight listener every form has copied for a decade. What field-sizing affects, and the HTML attributes it quietly switches off.