Custom progress element using the attr() function
Custom progress element using the attr() function ź“ė Ø
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>
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 */
}
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);
}
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;
}
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;
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 */
}
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.
Note
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));
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);
}
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;
);
}
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}
}
We will also be able to haveĀ a range syntax (w3c/csswg-drafts
)Ā 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}
}
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