Custom Functions and Mixins: A First Look at the New CSS Draft
For many of us, vanilla CSS has become increasingly capable thanks to features like custom properties and nesting, reducing our reliance on preprocessors. However, for tasks requiring reusable logic or looping, a tool like Sass still often feels necessary. A new spec draft may soon change that, as the CSS Working Group has adopted a proposal for native CSS @function and @mixin rules.
It's important to distinguish between these two new features. A custom function is designed to compute and return a single value, while a mixin is meant to encapsulate a block of reusable style rules. Both are identified with a dashed ident (e.g., --name), but they serve different purposes in your stylesheet.
The draft is still in its infancy, with many open questions and potential changes on the horizon. The editor of the spec, Miriam Suzanne, has also published a detailed explainer that fills in some of the early gaps, offering valuable context for those trying to understand the proposal.
Functions: The Next Step for Custom Properties
Custom functions are conceptually similar to custom properties, but instead of acting as a simple placeholder, they can contain logic and accept parameters to return an expected value. Consider the custom property syntax we're familiar with today:
:root {
--primary-color: hsl(25 100% 50%);
}
A custom function looks similar but uses an @function at-rule and definition parameters. The proposed syntax from the spec draft is:
@function <function-name> [( <parameter-list> )]? {
<function-rules>
result: <result>;
}
Inside the function, you define what the final result should be. A straightforward example from the spec calculates the area of a circle, taking a radius as its parameter:
@function --circle-area(--r) {
--r2: var(--r) * var(--r);
result: calc(pi * var(--r2));
}
Calling this function in your CSS is similar to using a custom property, but it requires arguments instead of relying on var():
.element {
inline-size: --circle-area(--r, 1.5rem); /* = ~7.065rem */
}
In its simplest form, you could achieve similar results with existing CSS features:
:root {
--r: 1rem;
--r2: var(--r) * var(--r);
--circle-area: calc(pi * var(--r2));
}
.element {
inline-size: var(--circle-area, 1.5rem);
}
However, the real power of a custom function is its ability to return different values based on conditions. For example, you could create a function that checks the viewport's inline size to determine which value to return, something a standard custom property can't easily do.
/* 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));
}
}
One current challenge is defining parameters as a comma-separated list because the CSSWG needs to ensure the syntax can't be mistaken for a compound selector.
Mixins: Encapsulating Reusable Style Blocks
For those familiar with Sass, the concept of a mixin will feel more straightforward. The new native @mixin at-rule is intended to hold style rules that you can then embed into other parts of your CSS, helping to keep your styles DRY. The syntax resembles functions, but the output is a set of style declarations.
/* Custom function */
@function <function-name> [( <parameter-list> )]? {
<function-rules>
result: <result>;
}
/* CSS/Sass mixin */
@mixin <mixin-name> [( <parameter-list> )]? {
<mixin-rules>
}
A classic use case for a mixin is a utility class, like the visually-hidden text styles we use for screen readers. Rather than cluttering your HTML with multiple utility classes, a mixin would allow you to define those styles once and apply them within a selector's rule set.
.sr-text {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
In a utility-class approach, you'd apply this class directly in the HTML:
<a class="sr-text">Skip to main content</a>
However, heavy reliance on utility classes can create verbose and difficult-to-read markup, as seen in examples from the Tailwind docs:
<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
Mixins offer an alternative: you "mixin-ize" the styles and then apply them within other CSS rules. The proposal introduces a new @apply at-rule to call the mixin, which is different from Sass's @include.
@mixin --sr-text {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
Instead of jumping into the HTML to apply the utility, you simply embed it in your main stylesheet like so:
header a:first-child {
@apply --sr-text;
/* Results in: */
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
This approach shines for common patterns, such as centering an element. You can define a mixin for it and then incorporate it wherever needed:
@mixin --center-me {
display: grid;
place-items: center;
}
This can then be used as part of a larger ruleset, allowing for cleaner separation of concerns. The mixin isn't limited to simple declarations; you can also define styles for pseudo-elements like ::before and ::after.
header {
@apply --center-me;
/*
display: grid;
place-items: center;
*/
background-color: --c-blue-50;
color: --c-white;
/* etc. */
}
The power increases when you introduce parameters to a mixin, enabling variations. For example, you could create a function for consistent gradients, passing different colors as arguments. The draft allows you to specify a syntax for each parameter, essentially providing type checking:
@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%;
}
}
You can also define default values for these parameters and use them as variable placeholders within the mixin's style rules.
@mixin --gradient-linear(--color-1, --color-2, --angle) {
/* etc. */
}
You can even include conditional logic, such as using @when/@else inside the mixin to adjust the output based on a set of conditions:
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
/* etc. */
}
Any number of these mixins can then be invoked with @apply in any desired ruleset, and they can even be combined with other mixins:
@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. */
}
[Note: The available placeholders have been incorporated as best as possible without unassigned references. The remaining content is intended to address open questions like looping in a declarative language, nesting conditions, scoping within @layer, and more.]
The proposal also raises several open questions that are still being discussed. These topics include handling mixins at the root level (outside of a selector), working with container queries—which may require setting global custom properties on an element other than the one being queried—and navigating type-checking nuances. A key point of debate is whether to support imperative flows like loops when CSS remains a declarative language, and what strategy for scoping mixins makes sense, perhaps using @layer or scope.
The early drafts and explainer provide a rich and complex overview. This space is rapidly evolving, and while the syntax and behavior are likely to change, the core ideas point toward a more modular, maintainable future for native CSS. For those tracking this development, the primary GitHub issue, background explainer, and community resources offer the best sources for following its progress.



