
Modern CSS: @function
, if()
and progress()
To me personally, CSS is the most exciting part of web development right now. Many great features were implemented in the past years and there are even more great features coming up and some of them are already available.
Let’s look at three new features that are now available in Chrome Canary and Safari Tech Preview.
Custom functions
CSS has many built-in functions like min()
, max()
, and clamp()
and now the CSS Functions and Mixins Module enables web developers to create custom functions.
Custom functions are similar to custom properties, but instead of returning a fixed value, functions calculate their return value based on parameters. Here’s an example of a function that negates a value:
@function --negative(--value) {
result: calc(-1 * var(--value));
}
It’s used like this:
div {
padding: --negative(1px); /* returns -1px */
}
I already wrote about custom functions in Modern Web Weekly #46 so if you want an in-depth explanation, be sure to read that edition.
So far, I have only seen quite simple examples of custom functions so in this edition, I want to give a more advanced example and for that, I will combine a custom function with two other upcoming features: if()
and progress()
.
if-statements in CSS
In Chrome Canary, you can now use the if()
function that enables you to use conditional logic in CSS:
section {
--theme: dark;
background-color: if(
style(--theme: dark): #cccccc;
else: #ffffff
);
}
Here we use a style query to read the value of —-theme
(the if-condition) and based on that, we return a color value (the declaration value). When the value of —-theme
is “dark” the value #cccccc is returned and otherwise #ffffff.
We can define more branches like else if
in other languages:
section {
--theme: dark;
background-color: if(
style(--theme: dark): #cccccc;
style(--theme: light): #ffffff;
else: transparent
);
}
In this example, the if-condition is a style query that returns a boolean value. Keep in mind that if-conditions always need to return a boolean value. We can also use other functions that return a boolean value, like supports()
and media()
.
The progress() function
progress()
takes three values: a progress value, a start value and an end value. It returns a number representing the position of the progress value between the start and end value.
For example, progress(4, 0, 10)
will return 0.4. The return value is the progress value divided by the end value, so when it’s between the start and end value, it will be between 0 and 1. When it’s greater than the end value, the same applies, so for example, progress(20, 0, 10)
will return 2 (20/10).
If you use progress()
to produce a pixel value, for example, you can use calc()
to convert it:
font-size: calc(progress(200, 0, 100) * 1px); // returns 2px
A background color-changing slider
Let’s combine @function(), if(), and progress() to create a slider with a background color that changes based on its value.
We can change the background color of a slider (input type=”range”) using the ::-webkit-slider-runnable-track
pseudo-element that, despite its name, doesn’t work in Safari but only in Chromium-based browsers. For the demo, this is fine because while progress()
works in Safari Tech Preview, if()
doesn’t yet.
When we set a background color on ::-webkit-slider-runnable-track
, its styling changes from the default styling, so we also need to set a height
and border-radius
and we also need to center the slider thumb element again with the pseudo-element ::-webkit-slider-thumb
:
::-webkit-slider-runnable-track {
height: 10px;
border-radius: 10px;
}
::-webkit-slider-thumb {
margin-top: -3px;
}
We’ll define the custom property ——value
to hold the current value of the slider and set an input event handler to keep track of it:
range.addEventListener('input', e => {
range.style.setProperty('--value', range.value);
});
Now I want the background color of the slider to change like this:
value < 20: green
value between 20 and < 60: yellow
value between 60 and < 80: orange
value between 80 and 100: red
We can use progress() to track when the current value passes the thresholds 20, 60, and 80 by using the same value for the start and end value. When the current value is below the threshold, it will be 0, and when it’s equal to or greater than the threshold, it will be 1 or higher:
/* returns 0 when --value < 20 and 1 or higher when value >= 20 */
progress(var(--value), 20, 20);
If we combine this with min(), it will return 0 when the value is less than 20 and 1 when the value is greater than or equal to 20:
/* returns 0 when --value < 20 and 1 when value >= 20 */
min(1, progress(var(--value), 20, 20));
Let’s wrap this in a function so we can reuse it:
@function --is-equal(--current, --threshold) {
result: min(1, progress(var(--value), var(--threshold),
var(--threshold)));
}
And then we can use it like this:
/* returns 0 when --value < 20 and 1 when value >= 20 */
--is-equal(var(--value), 20);
Now, let’s define three custom properties for the threshold values:
--after20: --is-equal(var(--value), 20);
--after60: --is-equal(var(--value), 60);
--after80: --is-equal(var(--value), 80);
Now these properties will be 0 or 1, depending on the current value in var(—-value)
.
If we use style queries to read the values of these custom properties, we can use that with an if()
statement to change the background-color
based on the current value:
&::-webkit-slider-runnable-track {
background-color: if(
style(--after80: 1): red;
style(--after60: 1): orange;
style(--after20: 1): yellow;
else: green
);
}
So this means that when the value of --after80
is 1, the if()
statement will return true
and the background-color will be red:
style(--after80: 1): red;
and so on for the other two.
But… when I first tried this approach, it didn’t work, even though I asserted that the custom properties indeed contained the correct value 1.
After some debugging, I realized that the issue is in the min()
function. While we use it to compare numbers, min()
can compare all kinds of types like <number>
, <length>
, <percentage>
etc., so the CSS engine doesn’t know what type of value the custom properties will hold, and the comparison will fail.
We can solve this by defining each custom property with @property
, and then it works:
@property --after20 {
syntax: "<number>"; /* specifies the type of the property */
inherits: true;
initial-value: 0;
}
But this is not very scalable, and you may not always be able to define each custom property you use like this, so a better solution is to use the optional return type declaration for @function
so the CSS engine knows what type it returns:
/* the return type now specifies the type of the return value */
@function --is-equal(--current, --threshold) returns <number> {
result: min(1, progress(var(--value), var(--threshold),
var(--threshold)));
}
And then everything works! 🎉
progress()
, if()
and @function
are supported in Chrome Canary, and progress()
is supported in Safari Tech Preview as well.
Check out the codepen demo.
All my Medium articles for $29
I write articles on Medium.com about PWAs, Web Components, and new and experimental browser features. To read these articles, you need to be a paid member of Medium ($60 per year) but I now offer all articles I wrote so far in PDF.
In these articles, you learn about PWAs, new and exciting browser features, Web Components, and other features to make great web apps. The articles contain a ton of code examples, YouTube videos, and codepens you can run and edit.
I also give my opinion and offer an overall analysis of the modern web platform in general. You will learn a LOT!
You can now get all these articles for only $29 with the discount code SUBSTACK.