Most days, I’m writing vanilla CSS. Thanks to CSS variables and nesting, I have fewer reasons to reach for Sass or any other preprocessor. The times I reach for Sass tend to be when I need a @mixin
to loop through a list of items or help keep common styles DRY.
That could change for me in the not-so-distant future since a new CSS Functions and Mixins Module draft was published in late June after the CSSWG resolved to adopt the proposal back in February.
Notice the module’s name: Functions and Mixins. There’s a distinction between the two.
This is all new and incredibly unbaked at the moment with plenty of TODO
notes in the draft and points to consider in future drafts. The draft spec doesn’t even have a definition for mixins yet. It’ll likely be some time before we get something real to work and experiment with, but I like trying to wrap my mind around these sorts of things while they’re still in early days, knowing things are bound to change.
In addition to the early draft spec, Miriam Suzanne published a thorough explainer that helps plug some of the information gaps. Miriam’s an editor on the spec, so I find anything she writes about this to be useful context.
There’s a lot to read! Here are my key takeaways…
Custom functions are advanced custom properties
We’re not talking about the single-purpose, built-in functions we’ve come to love in recent years — e.g., calc()
, min()
, max()
, etc. Instead, we’re talking about custom functions defined with an @function
at-rule that contains logic for returning an expected value.
That makes custom functions a lot like a custom property. A custom property is merely a placeholder for some expected value that we usually define up front:
:root {
--primary-color: hsl(25 100% 50%);
}
Custom functions look pretty similar, only they’re defined with @function
and take parameters. This is the syntax currently in the draft spec:
@function <function-name> [( <parameter-list> )]? {
<function-rules>
result: <result>;
}
The result
is what the ultimate value of the custom function evaluates to. It’s a little confusing to me at the moment, but how I’m processing this is that a custom function returns a custom property. Here’s an example straight from the spec draft (slightly modified) that calculates the area of a circle:
@function --circle-area(--r) {
--r2: var(--r) * var(--r);
result: calc(pi * var(--r2));
}
Calling the function is sort of like declaring a custom property, only without var()
and with arguments for the defined parameters:
.elenent {
inline-size: --circle-area(--r, 1.5rem); /* = ~7.065rem */
}
Seems like we could achieve the same thing as a custom property with current CSS features:
:root {
--r: 1rem;
--r2: var(--r) * var(--r);
--circle-area: calc(pi * var(--r2));
}
.element {
inline-size: var(--circle-area, 1.5rem);
}
That said, the reasons we’d reach for a custom function over a custom property are that (1) they can return one of multiple values in a single stroke, and (2) they support conditional rules, such as @supports
and @media
to determine which value to return. Check out Miriam’s example of a custom function that returns one of multiple values based on the inline size of the viewport.
/* Function name */
@function --sizes(
/* Array of possible values */
--s type(length),
--m type(length),
--l type(length),
/* The returned value with a default */
) returns type(length) {
--min: 16px;
/* Conditional rules */
@media (inline-size < 20em) {
result: max(var(--min), var(--s, 1em));
}
@media (20em < inline-size < 50em) {
result: max(var(--min), var(--m, 1em + 0.5vw));
}
@media (50em < inline-size) {
result: max(var(--min), var(--l, 1.2em + 1vw));
}
}
Miriam goes on to explain how a comma-separated list of parameters like this requires additional CSSWG work because it could be mistaken as a compound selector.
Mixins help maintain DRY, reusable style blocks
Mixins feel more familiar to me than custom functions. Years of writing Sass mixins will do that to you, and indeed, is perhaps the primary reason I still reach for Sass every now and then.
Mixins sorta look like the new custom functions. Instead of @function
we’re working with @mixin
which is exactly how it works in Sass.
/* Custom function */
@function <function-name> [( <parameter-list> )]? {
<function-rules>
result: <result>;
}
/* CSS/Sass mixin */
@mixin <mixin-name> [( <parameter-list> )]? {
<mixin-rules>
}
So, custom functions and mixins are fairly similar but they’re certainly different:
- Functions are defined with
@function
; mixins are defined with@mixin
but are both named with a dashed ident (e.g.--name
). - Functions
result
in a value; mixins result in style rules.
This makes mixins ideal for abstracting styles that you might use as utility classes, say a class for hidden text that is read by screenreaders:
.sr-text {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
In true utility fashion, we can sprinkle this class on elements in the HTML to hide the text.
<a class="sr-text">Skip to main content</a>
Super handy! But as any Tailwind-hater will tell you, this can lead to ugly markup that’s difficult to interpret if we rely on many utility classes. Screereader text isn’t in too much danger of that, but a quick example from the Tailwind docs should illustrate that point:
<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
It’s a matter of preference, really. But back to mixins! The deal is that we can use utility classes almost as little CSS snippets to build out other style rules and maintain a clearer separation between markup and styles. If we take the same .sr-text
styles from before and mixin-erize them (yep, I’m coining this):
@mixin --sr-text {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
Instead of jumping into HTML to apply the styles, we can embed them in other CSS style rules with a new @apply
at-rule:
header a:first-child {
@apply --sr-text;
/* Results in: */
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
Perhaps a better example is something every project seems to need: centering something!
@mixin --center-me {
display: grid;
place-items: center;
}
This can now be part of a bigger ruleset:
header {
@apply --center-me;
/*
display: grid;
place-items: center;
*/
background-color: --c-blue-50;
color: --c-white;
/* etc. */
}
That’s different from Sass which uses @include
to call the mixin instead of @apply
. We can even return larger blocks of styles, such as styles for an element’s ::before
and ::after
pseudos:
@mixin --center-me {
display: grid;
place-items: center;
position: relative;
&::after {
background-color: hsl(25 100% 50% / .25);
content: "";
height: 100%;
position: absolute;
width: 100%;
}
}
And, of course, we saw that mixins accept argument parameters just like custom functions. You might use arguments if you want to loosen up the styles for variations, such as defining consistent gradients with different colors:
@mixin --gradient-linear(--color-1, --color-2, --angle) {
/* etc. */
}
We’re able to specify the syntax for each parameter as a form of type checking:
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
/* etc. */
}
We can abstract those variables further and set default values on them:
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
--from: var(--color-1, orangered);
--to: var(--from-color, goldenrod);
--angle: var(--at-angle, to bottom right);
/* etc. */
}
…then we write the mixin’s style rules with the parameters as variable placeholders.
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
--from: var(--color-1, orangered);
--to: var(--from-color, goldenrod);
--angle: var(--at-angle, to bottom right);
background: linear-gradient(var(--angle), var(--from), var(--to));
}
Sprinkle conditional logic in there if you’d like:
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
--from: var(--color-1, orangered);
--to: var(--from-color, goldenrod);
--angle: var(--at-angle, to bottom right);
background: linear-gradient(var(--angle), var(--from), var(--to));
@media (prefers-contrast: more) {
background: color-mix(var(--from), black);
color: white;
}
}
This is all set to @apply
the mixin in any rulesets we want:
header {
@apply --gradient-linear;
/* etc. */
}
.some-class {
@apply --gradient-linear;
/* etc. */
}
…and combine them with other mixins:
header {
@apply --gradient-linear;
@apply --center-me;
/* etc. */
}
This is all very high level. Miriam gets into the nuances of things like:
- Applying mixins at the root level (i.e., not in a selector)
- Working with Container Queries with the limitation of having to set global custom properties on another element than the one that is queried.
- The possibility of conditionally setting mixin parameters with something like
@when
/@else
in the mixin. (Which makes me wonder about the newly-proposedif()
function and whether it would be used in place of@when
.) - Why we might draw a line at supporting loops the same way Sass does. (CSS is a declarative language and loops are imperative flows.)
- Scoping mixins (
@layer
?scope
? Something else?)
Miriam has an excellent outline of the open questions and discussions happening around mixins.
That’s, um, it… at least for now.
Gah, this is a lot for my blonde brain! Anytime I’m neck-deep in CSS specification drafts, I have to remind myself that the dust is still settling. The spec authors and editors are wrestling with a lot of the same questions we have — and more! — so it’s not like a cursory read of the drafts is going to make experts out of anyone. And that’s before we get to the fact that things can, and likely will, change by the time it all becomes a recommended feature for browsers to implement.
This will be an interesting space to watch, which is something you can do with the following resources: