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 propertiesWe’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:
.element { 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 blocksMixins 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:
@function; mixins are defined with @mixin but are both named with a dashed ident (e.g. --name).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:
```