Custom progress element using the attr() function

Temani Afif Temani Afif on

In a previous article, we combined two modern CSS features (anchor positioning and scroll-driven animations) to style the <progress> element without extra markup and create a cool component. Here’s that demo:

Anchor positioning was used to correctly place the tooltip shape while scroll-driven animations were used to get the progress value and show it inside the tooltip. Getting the value was the trickiest part of the experimentation. I invite you to read the previous article if you want to understand how scroll-driven animations helps us do it.

In this article, we will see an easier way to get our hands on the current value and explore another example of progress element.

At the time of writing, only Chrome (and Edge) have the full support of the features we will be using.

Article Series

Getting the progress value using attr()

This is the HTML element we are working with:

<progress value="4" max="10"></progress>Code language: HTML, XML (xml)

Nothing fancy: a progress element where you define the value and max attribute. Then we use the following CSS:

progress[value] {
  --val: attr(value type(<number>));
  --max: attr(max type(<number>),1);

  --x: calc(var(--val)/var(--max)); /* the percentage of progression */
}Code language: CSS (css)

We waited for this for too long! It’s finally here!

We can use attr() function not only with the content property but with any property including custom properties! The variable --x will contain the percentage of progression as a unit-less value in the range [0 1]. That’s all — no complex code needed.

We also have the ability to define the types (number, in our case) and specify fallback values. The max attribute is not mandatory so if not specified it will default to 1. Here is the previous demo using this new method instead of scroll-driven animations:

If we omit the tooltip and animation parts (explained in the previous article), the new code to get the value and use it to define the content of the tooltip and the color is a lot easier:

progress {
  --val: attr(value type(<number>));
  --max: attr(max type(<number>),1);

  --x: calc(100*var(--val)/var(--max));
  --_c: color-mix(in hsl,#E80E0D,#7AB317 calc(1%*var(--x)));
}
progress::value {
  background: var(--_c);
}
progress::before {
  content: counter(val) "%";
  counter-reset: val var(--x);
  background: var(--_c);
}Code language: CSS (css)

Should we forget about the “complex” scroll-driven animations method?

Nah — it can still be useful. Using attr() is the best method for this case and probably other cases but scroll-driven animations has one advantage that can be super handy: It can make the progress value available everywhere on the page.

I won’t get into the detail (as to not repeat the previous article) but it has to do with the scope of the timeline. Here is an example where I am showing the progress value within a random element on the page.

The animation is defined on the html element (the uppermost element) which means all the elements will have access to the --x variable.

If your goal is to get the progress value and style the element itself then using attr() should be enough but if you want to make the value available to other elements on the page then scroll-driven animations is the key.

Progress element with dynamic coloration

Now that we have our new way to get the value let’s create a progress element with dynamic coloration. This time, we will not fade between two colors like we did in the previous demo but the color will change based on the value.

A demo worth a thousand words:

As you can see, we have 3 different colors (red, orange and green) each one applied when the value is within a specific range. We have a kind of conditional logic that we can implement using various techniques.

Using multiple gradients

I will rely on the fact that a gradient with a size equal to 0 will be hidden so if we stack multiple gradients and control their visibility we can control which color is visible.

progress[value] {
  --val: attr(value type(<number>));
  --max: attr(max type(<number>),1);
  --_p: calc(100%*var(--val)/var(--max)); /* the percentage of progression */
}
progress[value]::-webkit-progress-value {
   background: 
    /* if (p < 30%) "red" */
    conic-gradient(red    0 0) 0/max(0%,30% - var(--_p)) 1%,
    /* else if (p < 60%) "orange" */
    conic-gradient(orange 0 0) 0/max(0%,60% - var(--_p)) 1%,
    /* else "green" */
    green;
}Code language: CSS (css)

We have two single-color gradients (red and orange) and a background-color (green). If, for example, the progression is equal to 20%, the first gradient will have a size equal to 10% 1% (visible) and the second gradient will have a size equal 40% 1% (visible). Both are visible but you will only see the top layer so the color is red. If the progression is equal to 70%, both gradients will have a size equal to 0% 1% (invisible) and the background-color will be visible: the color is green.

Clever, right? We can easily scale this technique to consider as many colors as you want by adding more gradients. Simply pay attention to the order. The smallest value is for the top layer and we increase it until we reach the bottom layer (the background-color).

Using an array of colors

A while back I wrote an article on how to create and manipulate an array of colors. The idea is to have a variable where you can store the different colors:

--colors: red, blue, green, purple;Code language: CSS (css)

Then be able to select the needed color using an index. Here is a demo taken from that article.

This technique is limited to background coloration but it’s enough for our case.

This time, we are not going to define precise values like we did with the previous method but we will only define the number of ranges.

  • If we define N=2, we will have two colors. The first one for the range [0% 50%[ and the second one for the range [50% 100%]
  • If we define N=3, we will have three colors. The first one for [0% 33%[, the second for [33% 66%[ and the last one for [66% 100%]

I think you get the idea and here is a demo with four colors:

The main trick here is to convert the progress value into an index and to do this we can rely on the round() function:

progress[value] {
  --n: 4; /* number of ranges */
  --c: #F04155,#F27435,#7AB317,#0D6759;
  
  --_v: attr(value type(<number>));
  --_m: attr(max type(<number>),1);
  --_i: round(down,100*var(--_v)/var(--_m),100/var(--n)); /* the index */
}Code language: CSS (css)

For N=4, we should have 4 indexes (0,1,2,3). The 100*var(--_v)/var(--_m) part is a value in the range [0 100] and 100/var(--n) part is equal to 25. Rounding a value to 25 means it should be a multiplier of 25 so the value will be equal to one of the following: 0, 25, 50, 75, 100. Then if we divide it by 25 we get the indexes.

But we have 5 indexes and not 4.

True, the value 100 alone will create an extra index but we can fix this by clamping the value to the range [0 99]

--_i: round(down,min(99,100*var(--_v)/var(--_m)),100/var(--n));Code language: CSS (css)

If the progress is equal to 100, we will use 99 because of the min() and the round will make it equal to 75. For the remaining part, check my other article to understand how I am using a gradient to select a specific color from the array we defined.

progress[value]::-webkit-progress-value {
   background:
     linear-gradient(var(--c)) no-repeat
     0 calc(var(--_i)*var(--n)*1%/(var(--n) - 1))/100% calc(1px*infinity);
}Code language: CSS (css)

Using an if() condition

What we have done until now is a conditional logic based on the progress value and CSS has recently introduced inline conditionals using an if() syntax.

The previous code can be written like below:

@property --_i {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}
progress[value] {
  --n: 4; /* number of ranges */
  
  --_v: attr(value type(<number>));
  --_m: attr(max type(<number>),1);
  --_i: calc(var(--n)*round(down,min(99,100*var(--_v)/var(--_m)),100/var(--n))/100); 
}
progress[value]::-webkit-progress-value {
   background: if(
     style(--_i: 0): #F04155;
     style(--_i: 1): #F27435;
     style(--_i: 2): #7AB317;
     style(--_i: 3): #0D6759;
    );
}Code language: CSS (css)

The code is self-explanatory and also more intuitive. It’s still too early to adopt this syntax but it’s a good time to know about it.

Using Style Queries

Similar to the if() syntax, we can also rely on style queries and do the following:

@property --_i {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}
progress[value] {
  --n: 4; /* number of ranges */
  
  --_v: attr(value type(<number>));
  --_m: attr(max type(<number>),1);
  --_i: calc(var(--n)*round(down,min(99,100*var(--_v)/var(--_m)),100/var(--n))/100); 
}
progress[value]::-webkit-progress-value {
  @container style(--_i: 0) {background-color: #F04155}
  @container style(--_i: 1) {background-color: #F27435}
  @container style(--_i: 2) {background-color: #7AB317}
  @container style(--_i: 3) {background-color: #0D6759}
}Code language: CSS (css)

We will also be able to have a range syntax and the code can be simplified to something like the below:

@property --_i {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}
progress[value] {
  --_v: attr(value type(<number>));
  --_m: attr(max type(<number>),1);
  --_i: calc(var(--_v)/var(--_m)); 
}
progress[value]::-webkit-progress-value {
  background-color: #0D6759;
  @container style(--_i < .75) {background-color: #7AB317}
  @container style(--_i < .5 ) {background-color: #F27435}
  @container style(--_i < .25) {background-color: #F04155}
}Code language: CSS (css)

This is also something “in progress” so know about it but don’t rely on it yet as things may change.

Conclusion

I hope this article and the previous one give you a good overview of what modern CSS looks like. We are far from the era of simply setting color: red and margin: auto. Now, it’s a lot of variables, calculations, conditional logic, and more!

Article Series

Wanna learn CSS from a course?

Frontend Masters logo

FYI, we have a full CSS learning path with multiple courses depending on how you want to approach it.

7-Day Free Trial

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.