A much-needed disclaimer: You (kinda) can use functions now! I know, it isn’t the most pleasant feeling to finish reading about a new feature just for the author to say “And we’ll hopefully see it in a couple of years”. Luckily, right now you can use an (incomplete) version of CSS functions in Chrome Canary behind an experimental flag, although who knows when we’ll get to use them in a production environment.
Arguments, defaults, and returns!
I was drinking coffee when I read the news on Chrome prototyping functions in CSS and… I didn’t spit it or anything. I was excited, but thought “functions” in CSS would be just like mixins in Sass — you know, patterns for establishing reusable patterns. That’s cool but is really more or less syntactic sugar for writing less CSS.
But I looked at the example snippet a little more closely and that’s when the coffee nearly came shooting out my mouth.
From Bramus in Bluesky
Arguments?! Return values?! That’s worth spitting my coffee out for! I had to learn more about them, and luckily, the spec is clearly written, which you can find right here. What’s crazier, you can use functions right now in Chrome Canary! So, after reading and playing around, here are my key insights on what you need to know about CSS Functions.
What exactly is a function in CSS?
I like this definition from the spec:
Custom functions allow authors the same power as custom properties, but parameterized
They are used in the same places you would use a custom property, but functions return different things depending on the argument we pass. The syntax for the most basic function is the @function
at-rule, followed by the name of the function as a <dashed-ident>
+ ()
@function --dashed-border()
/* ... */
A function without arguments is like a custom property, so meh… To make them functional we can pass arguments inside the parenthesis, also as <dashed-ident>
s
@function --dashed-border(--color)
/* ... */
We can use the result
descriptor to return something based on our argument:
@function --dashed-border(--color)
result: 2px dashed var(--color);
div
border: --dashed-border(blue); /* 2px dashed blue */
We can even use defaults! Just write a colon (
:
) followed by the default value for that argument.@function --dashed-border(--color: red) result: 2px dashed var(--color); div border: --dashed-border(); /* 2px dashed red */
This reminds me of Adam Argyle’s experiment on a functional CSS concept.
Functions can have type-checking
Functions can have type-checking for arguments and return values, which will be useful whenever we want to interpolate a value just like we do with variables created with @property
, and once we have inline conditionals, to make different calculations depending on the argument type.
To add argument types, we pass a syntax component. That is the type enclosed in angle brackets, where color is <color>
and length is <length>
, just to name a couple. There are also syntax multipliers like plus (+
) to accept a space-separated list of that type.
@function --custom-spacing(--a <length>) /* ... */ /* e.g. 10px */
@function --custom-background(--b <color>) /* ... */ /* e.g. hsl(50%, 30% 50%) */
@function --custom-margin(--c <length>+) /* ... */ /* e.g. 10px 2rem 20px */
If instead, we want to define the type of the return value, we can write the returns
keyword followed by the syntax component:
@function --progression(--current, --total) returns <percentage>
result: calc(var(--current) / var(--total) * 100%);
Just a little exception for types: if we want to accept more than one type using the syntax combinator (|), we’ll have to enclose the types in a type()
wrapper function:
@function --wideness(--d type(<number> | <percentage>)) /* ... */
Functions can have list arguments
While it doesn’t currently seem to work in Canary, we’ll be able in the future to take lists as arguments by enclosing them inside curly braces. So, this example from the spec passes a list of values like 1px, 7px, 2px
and gets its maximum to perform a sum.
@function --max-plus-x(--list, --x)
result: calc(max(var(--list)) + var(--x));
div
width: --max-plus-x( 1px, 7px, 2px , 3px); /* 10px */
I wonder then, will it be possible to select a specific element from a list? And also define how long should the list should be? Say we want to only accept lists that contain four elements, then select each individually to perform some calculation and return it. Many questions here!
Early returns aren’t possible
That’s correct, early returns aren’t possible. This isn’t something defined in the spec that hasn’t been prototyped, but something that simply won’t be allowed. So, if we have two returns, one enclosed early behind a @media
or @supports
at-rule and one outside at the end, the last result will always be returned:
@function --suitable-font-size()
@media (width > 1000px)
result: 20px;
result: 16px; /* This always returns 16px */
We have to change the order of the returns, leaving the conditional result
for last. This doesn’t make a lot of sense in other programming languages, where the function ends after returning something, but there is a reason the C in CSS stands for Cascade: this order allows the conditional result to override the last result which is very CSS-y is nature:
@function --suitable-font-size()
result: 16px;
@media (width > 1000px)
result: 20px;
Imagining the possibilities
Here I wanted everyone to chip in and write about the new things we could make using functions. So the team here at CSS-Tricks put our heads together and thought about some use cases for functions. Some are little helper functions we’ll sprinkle a lot throughout our CSS, while others open new possibilities. Remember, all of these examples should be viewed in Chrome Canary until support expands to other browsers.
Here’s a basic helper function from Geoff that sets fluid type:
@function --fluid-type(--font-min, --font-max)
result: clamp(var(--font-min), 4vw + 1rem, var(--font-max));
h2
font-size: --fluid-type(24px, 36px);
This one is from Ryan, who is setting the width with an intrinsic container function — notice the default arguments.
@function --intrinsic-container(--inline-margin: 1rem, --max-width: 60ch)
result: min(100% - var(--inline-margin), var(--max-width));
And check out this second helper function from Ryan to create grid layouts:
@function --layout-sidebar(--sidebar-width: 10ch) result: 1fr; @media (width > 640px) result: fit-content(var(--sidebar-width)) minmax(min(50vw, 30ch), 1fr);
This is one of those snippets I’m always grabbing from Steph Eckles’ smolcss site, and having a function would be so much easier. Actually, most of the snippets on Steph’s site would be awesome functions.
This one is from moi. When I made that demo using tan(atan2())
to create viewport transitions, I used a helper property called --wideness
to get the screen width as a decimal between 0
to 1
. At that moment, I wished for a function form of --wideness
. As I described it back then:
You pass a lower and upper bound as pixels, and it will return a
0
to1
value depending on how wide the screen is. So for example, if the screen is800px
,wideness(400px, 1200px)
would return0.5
since it’s the middle point
I thought I would never see it, but now I can make it myself! Using that wideness function, I can move an element through its offset-path
as the screen goes from 400px
to 800px
:
.marker
offset-path: path("M 5 5 m -4, 0 a 4,4 0 1,0 8,0 a 4,4 0 1,0 -8,0"); /* Circular Orbit */
offset-distance: calc(--wideness(400, 800) * 100%); /* moves the element when the screen goes from 400px to 800px */
What’s missing?
According to Chrome’s issue on CSS Functions, we are in a super early stage since we cannot:
- …use local variables. Although I tried them and they seem to work.
- …use recursive functions (they crash!),
- …list arguments,
- …update a function and let the appropriate styles change,
- …use
@function
in cascade layers, or in the CSS Object Model (CSSOM), - …use “the Iverson bracket functions … so any
@media
queries or similar will need to be made using helper custom properties (on:root
or similar).”
After reading what on earth an Iverson bracket is, I understood that we currently can’t have a return value behind a @media
or @support
rule. For example, this snippet from the spec shouldn’t work:
@function --suitable-font-size()
result: 16px;
@media (width > 1000px)
result: 20px;
Although, upon testing, it seems like it’s supported now. Still, we can use a provisional custom property and return it at the end if it isn’t working for you:
@function --suitable-font-size()
--size: 16px;
@media (width > 600px)
--size: 20px;
result: var(--size);
What about mixins? Soon, they’ll be here. According to the spec:
At this time, this specification only defines custom functions, which operate at the level of CSS values. It is expected that it will define “mixins” later, which are functions that operate at the style rule level.
In conclusion…
I say it with confidence: functions will bring an enormous change to CSS, not in the sense that we’ll write it any differently — we won’t use functions to center a <div>
, but they will simplify hack-ish CSS and open a lot of new possibilities. There’ll be a time when our cyborg children ask us from their education pods, “Is it true you guys didn’t have functions in CSS?” And we’ll answer “No, Zeta-5 ∀umina™, we didn’t” while shedding a tear. And that will blow their ZetaPentium© Gen 31 Brain chips. That is if CSS lasts long enough, but in the meantime, I am happy to change my site’s font with a function.