progress()

The CSS progress() functions tells us where a value is relative to a minimum and a maximum. Specifically, given a value along a lower and upper bound, progress() returns a number between 0 and 1 that represents the value’s position within that range.

In the following example, we use progress() to know where the viewport width (100vw) is within the 400px to 1200px range, which we can use — among lots of things — to rotate an element accordingly.

img {
  rotate: calc(progress(100vw, 400px, 1200px) * 720deg);
}

The progress() function is defined in the CSS Values and Units Module Level 5 specification.

Syntax

progress(<calc-sum>, <calc-sum>, <calc-sum>)

The progress() functions takes three arguments, all of them <calc-sum>s that must resolve to either a <number><dimension><angle> or <percentage>.

The formal syntax doesn’t tell us much, so we can better write it as:

progress(value, start, end)

This is unlike the CSS clamp() function, where the lower and upper bounds are the first and third arguments, respectively.

Arguments

/* Basic usage */
progress(2, -5, 5)
progress(5s, 1000ms, 10s);

/* It's best used with variable values  */
progress(20%, 12px, 18px);
progress(80vw, 400px, 500px);

/* We can do math inside progress() -- no need for calc()! */
progress(2rem + 1lh, 50px, 100px);
progress(360deg * 4, 0.5turn, 5turn);

The progress() function takes three arguments:

  • value: The value within the range
  • start: The lower bound of the range
  • end: The upper bound of the range

Each of those arguments can have different units, but they must be from the same type. For example, the next expression is valid since all units are of type :

progress(5rem, 100px, 4in) /* Valid! */

But the next one isn’t, since we are mixing and types:

progress(5, 20deg, 45deg) /* NOT Valid! */

If the value is below or above the range, it returns 0 or 1, respectively.

Basic usage

With progress(), we can reduce a value within a range to a number between 0 to 1, which is way more versatile to work with. Take, for example, the following demo where we have a block of text on the left and an image on the right. It doesn’t reflow nicely as the screen gets smaller:

On bigger screens, both elements remain on their sides, but as the viewport shrinks, they start to overlap, making the text almost impossible to read. One solution could be to reduce the image’s opacity as the viewport shrinks, but how could we change the opacity (a number/percentage) relative to the viewport (a length)?

Luckily, progress() makes this easy since we can position the viewport width (100vw) relative to a lower and upper bound:

img {
  opacity: progress(100vw, 400px, 1200px);
}

This will go from 0 when 100vw is equal to or smaller than 400px, all the way to 1 when it’s equal to or bigger than 1200px.

However, we don’t want our image to become too transparent. To solve this, we can cap it using the CSS max() function so that it never goes below an opacity of 0.4:

img {
   opacity: max(0.4, progress(100vw, 400px, 1200px));
}

progress() is more than syntactical sugar

The progress() function works under the following formula. In a nutshell, given a range, it normalizes any value within 0 and 1:

(value - start) / (end - start)

If the value is below the lower or above the upper limit, then it’s capped to 0 and 1.

However, don’t let it make you think that progress() is just syntactical sugar for the formula above, since it also lets us typecast different units onto a <number>. If we look back at our first example and try to rewrite it without progress()

img {
  opacity: clamp(0.4, (100vw - 400px) / (1200px - 400px), 1); /* Doesn't work! */
}

We’ll notice it doesn’t work! This is because we can’t divide two px units into a <number>, so the whole declaration becomes invalid. However, progress() does this type conversion implicitly, so we can easily go from a <length> to a <number>.

Unless your browser supports typed arithmetic, then it should work fine.

Changing colors

If progress() allows us to go from any unit to a <number>, then using <number> as a starting point, we can go to any other unit we want!

Take the next loader as an example:

Right now, the ring color is always coral, but let’s say we wanted to change its hue from red to green based on the current <percentage>. Our first step will be to transform the percentage (--percentage) to a number between 0 to 1 using progress(). Once there, we can multiply it by an <angle> to use it as the color’s hue.

.loader {
  --hue: calc(progress(var(--percentage), 0%, 100%) * 120deg);
}

I choose 120deg since that’s the distance between red (0°) and green (120°) in the HSL color wheel:

And from here, we can use the --hue to change the color inside hsl()!

.loader {
  /* ... */
  --bg-color: hsl(var(--hue) 100 70);
}

Getting the sibling-index() progress

Lastly, even if we are not typecasting across different units, it doesn’t mean we can’t take advantage of the progress() syntax, even if it sometimes is just syntactical sugar.

Imagine we have the next list of user icons. This works best in Chrome as I’m writing this:

And let’s say we want to apply some effect based on each element’s position on the sibling list (i.e., if it’s first, second, third, etc.), maybe a blur effect that gets heavier the further back each element is. Using progress(), we can get each element’s relative position like this:

progress(sibling-index(), 1, sibling-count())

While sibling-index() and sibling-count() are unitless—so we could use progress() formula, this gives us a prettier syntax.

Once we have our progress() set up, we can multiply it by a unit to get the desired blur() effect.

li {
  --effect: calc(progress(sibling-index(), 1, sibling-count()) * 1px);
  filter: blur(var(--effect));
}

Again, this works best in Chrome:

Edge cases

Usually, we won’t encounter any unexpected result while working with progress(), but if for any reason the start and end values are the same, then it could return:

  • 0 if the progress value is also the same
  • -Infinity if the progress value is smaller
  • Infinity if the progress value is bigger

If you have any doubts about progress() behavior, you can always go back to its formula:

(value - start) / (end - start)

Specification

The progress() function is defined in the CSS Values and Units Module Level 5 specification, which is currently in Editor’s Draft. That means the information can change between now and when it officially becomes a stable Candidate Recommendation.

Browser support

More information