ayered Toggles: Optional CSS Mixins
In this article, I am sharing the next evolution of space & cyclic toggles, which allows us to create and apply optional mixins in CSS with the help of custom cascade layers, available today in every browser that supports them.
I am no stranger to looking for various ways to apply some CSS conditionally, and through the years, went from using math as a way to achieve conditions for CSS variables back in 2016, to cyclic dependency space toggles that I came up with last year.
This was one of the reasons why I found another missing piece for this puzzle of having conditions in CSS, as an experiment in reply to Heydon Pickering’s question in Mastodon, which I could’ve potentially missed if not for Nathan Knowler mentioning my cyclic toggles article.
While the technique I discovered did not help Heydon with his case, I found this discovery to be important enough to share as fast as I could. So here we are.
he Technique
This technique, which I named “layered toggles”, essentially allows us to apply any number of CSS mixins on any element!
Let me show you a minimal exampleGo to a sidenote, and afterward, I will explain what’s going on.
@layer default {
.item {
padding: 1em;
border-radius: 0.5em;
background: var(--PINK);
}
}
@layer mixins {
* {
--hover: var(--hover--off);
--hover--off: var(--hover,);
--hover--on: var(--hover,);
background-color:
var(--hover--off, revert-layer)
var(--hover--on, var(--GREEN));
}
}
/* Applying mixin */
.can-be-hovered:is(:hover, :focus-visible),
.pseudo-hovered {
--hover: var(--hover--on);
}
The first thing you might notice: we’re using custom cascade layers. This is the main requirementGo to a sidenote for the technique to work, specifically the revert-layer
CSS-wide keyword.
I started playing with it after reading Nathan’s “So, You Want to Encapsulate Your Styles?” article, as well as Mayank’s “Some use cases for revert-layer” one, specifically “the self-reset” section, but, until now, I did not think of combining layers with the space or cyclic toggles.
But, this time, I tried to useGo to a sidenote the revert-layer
as one of the cyclic toggle’s values — and it did work!
So, here is what is going on:
-
We need to separate our styles into two layers: the lower one will contain the “default” styles, and the higher layer will contain our “mixins”. This allows mixins to override the default styles when they are applied.
-
In our mixins, we are using “Cyclic Dependency Space Toggles” to define our styles, with the important part being
revert-layer
applied as the default value. Due to how it works, this allows us to “return control” over the property to the lower layers unless our “condition” (enabling some mixin) will happen. -
Finally, our set-up is complete, and we can “use” our mixin by switching the value of our cyclic toggle anywhere in CSS. As soon as it is enabled — based on some selector, media query, inline styles, or anything else — the mixin’s styles will override the default styles.
And here we are — with the ability to apply some styles conditionally based on a single CSS variable on any element, but without messing up our default styles.
I wanted to have this as a feature for years, probably since I first saw Lea Verou define pseudo-mixins using the universal selector in one of her CSS Variables talks. But when the styles were inside just a rule with a universal selector, that led to any later rules that touched this property to override our mixin’s styles. And if we do anything with this, our mixin could become too powerful and go over everything. Layered toggles don’t have this problem.
imitations
Of course, this technique is not without its limitations:
-
All mixins live in a shared space. This means: that if multiple mixins need to toggle the same CSS property, we will need to define how this should be handled. In the next section, I will show two ways to do it.
-
As we’re applying our styles through a universal selectorGo to a sidenote (
*
), we could use the mixin to only style the element itself, not its children. However, it will be possible to work around this with style queries, I’ll describe how later in the article. This might also be a blessing in some cases, as we don’t want to apply mixin to the nested elements, so the disabled inheritance here can be welcome. -
We cannot toggle other cyclic toggles with this. Or, at least, I did not find a way to do so yet.
-
Obviously, it requires the CSS layers to be supported, and all main styles to be handled by CSS layers. This means that you might wait to apply this technique unless you are certain that you can use custom cascade layers with the browser support your website requires.
-
It is unknown how bad this technique could be for performance with many mixins. Browsers will attempt to apply the mixins for every element, going through multiple layers until encountering the one that has its variables enabled. Proper performance evaluation should be done.
Because this technique is so new, there is a big chance I’m missing some other limitations or potential problems — if you encounter any, please let me know, and I will include them in this article.
dvanced Cases
ultiple Mixins
What if we want to have two mixins, and both of them would like to apply some background
? Which should win? How do we define this?
Thankfully, as we’re already using custom cascade layers, we can continue doing just that and rely on their cascading nature!
Nothing stops us from separating every mixin into its sub-layer, making it possible for every mixin to handle its list of properties, and revert any of its layered values to either some other previously defined mixin or to the default values on the element.
Here is the CSS for this example:
@layer mixins.alpha {
* {
--mix-alpha: var(--mix-alpha--off);
--mix-alpha--off: var(--mix-alpha,);
--mix-alpha--on: var(--mix-alpha,);
--mix-alpha-color: currentColor;
--mix-alpha-value: 75%;
color:
var(--mix-alpha--off, revert-layer)
var(--mix-alpha--on,
color-mix(
in srgb,
var(--mix-alpha-color)
var(--mix-alpha-value),
transparent
)
);
}
}
@layer mixins.red {
* {
--mix-red: var(--mix-red--off);
--mix-red--off: var(--mix-red,);
--mix-red--on: var(--mix-red,);
background:
var(--mix-red--off, revert-layer)
var(--mix-red--on, var(--RED));
color:
var(--mix-red--off, revert-layer)
var(--mix-red--on, #FFF);
text-shadow:
var(--mix-red--off, revert-layer)
var(--mix-red--on, none);
}
}
/* Applying mixins */
.with-alpha {
--mix-alpha: var(--mix-alpha--on);
&:is(:hover, :focus-visible) {
--mix-alpha-value: 90%;
}
}
.is-red {
--mix-red: var(--mix-red--on);
}
A few notes:
-
For the third element, both
with-alpha
andis-red
classes are applied. But because themixins.red
layer comes after themixins.alpha
, its properties “win”. -
The mixins are completely separate and both can override the
color
, but either can apply only based on its custom property! -
Nothing stops us from using additional variables that we can pass to the mixin’s code. I did not guard them against other mixins, but it is also possible to wrap them with the cyclic toggles conditions if necessary.
erging Mixins
You could’ve noticed that right now, either mixin does not know about the other, making it so the red
one completely overrides the alpha
. Can we somehow make them work together?
There are many ways to handle this, but the one I find the best is to modify the usage of the later mixin to fall back to the earlier if it detects its state. This way, it is possible to conditional revert-layer
to get the values from the earlier mixin in place:
Here is what is different:
@layer mixins.red {
* {
color:
var(--mix-red--off, revert-layer)
var(--mix-red--on,
var(--mix-alpha--off, #FFF)
var(--mix-alpha--on, revert-layer)
);
--mix-alpha-color:
var(--mix-red--off, revert-layer)
var(--mix-red--on, #FFF);
}
}
-
When our
red
mixin is on, we also check the other mixin: if it is enabled, then we know that we can rely on it. -
But how do we change the color to white? Easy: the other mixin provides a
--mix-alpha-color
variable that we can use!
Note how here either mixin knows about what the other is doing: the alpha
gets the value of --mix-alpha-color
that the red
defines, and the red
knows that the alpha
is active, and reverts to its handling of color
!
Also, when testing all the examples in Safari, I found a bug in WebKit with the revert-layer
when used in a nested variable fallback, which leads to hover in the above example not working in it. I’ll fill a big about this at a later point and will update this paragraph to include a link to it.
sing With Style Queries
You could’ve asked: why are we not using container style queries for this? There are two main answers:
-
Style queries are not supported everywhere: when I write this, they’re only available in stable Chromium-based browsers, and in Safari Technology Preview.
-
Even if they were available everywhere, it is not possible to style the element with a CSS variable itself with them — only the descendants.
However, when we get style queries everywhere, could we use them in tandem with this technique:
Hovering on any element, Here is the CSS for this example:
@layer mixins.child-hover {
* {
--mix-child-hover: var(--mix-child-hover--off);
--mix-child-hover--off: var(--mix-child-hover,);
--mix-child-hover--on: var(--mix-child-hover,);
/* Applies only to the direct children of the element */
@container style(--mix-child-hover--off: ) {
background: var(--GREEN);
color: CanvasText;
}
}
}
/* Applying mixin */
.child-hover:is(:hover, :focus-visible):not(:has(.child-hover:is(:hover, :focus-visible))) {
--mix-child-hover: var(--mix-child-hover--on);
}
We can hook into the value of either --mix-child-hover--on
or --mix-child-hover--off
, and style this element’s direct child based on its parent’s cyclic toggle value, as it will be either an empty value or initial
.
Note that in Safari Technology Preview there is a bug with the initial
inside the style queries, so in the example above we’re checking for style (--mix-child-hover--off:)
; in Chrome the style (--mix-child-hover--on: initial)
will work as well.
se Cases
The examples above are very basic, mostly because I wanted to share them as soon as possible. I could probably spend a few weeks coming up with many examples, but I feel that there might be too many of them. Almost anything that you could want a native CSS mixin could be implemented with layered toggles, I believe.
That might be your homework: think of anything reusable in your CSS — could it be separated into a mixin like that? I will be happy to look at what all of you will do with this.
There are many articles on the internet which you could use as an inspiration, with small snippets of code that can be converted into mixins like that. I can recommend starting with SmolCSS by Stephanie Eckles, and CSS Tip by Temani Afif. There are many, many other resources, so don’t stop at these!
pdate from 2024-04-10
-
I did create a CodePen that combines Stephanie’s flex and grid layouts into one mixin.
The flex one requires style queries to work, but the grid one will work everywhere where
@layer
is supported! We can see how the code is hardly readable, but it works! One interesting moment to note is that if we’d want to use some variables on the children that are defined on the parent, we’d need to explicitlyinherit
these variables, as otherwise they will be reset on every element due to the universal selector. -
And another example: a CodePen for visually-hidden mixin, based on the code from the “The Web Needs a Native .visually-hidden” article by “Ben Myers”, inspired by a “Native visually hidden” post by Matthias Zöchling.
This one should work everywhere where we have
@layer
.
uture of Mixins
While I’m happy this technique is possible, it can still look rather hacky, and cyclic toggles are not very fun to debug right now. Good news: there is a Custom CSS Functions & Mixins proposal by Miriam Suzanne which was accepted by CSSWG as something to pursue. However, it is yet unknown which exact form it will take, and it can take years until we could use whatever will get into the specifications and later into browsers. CSS Layers are already here, and style queries will be sooner rather than later too, and the best thing we could do for the future of mixins — is prototype them with what we can today. This could allow us to gather common use cases, uncover potential issues, work out how they could interact with other CSS features, and so on.
inal Words
I wrote this article very soon after discovering the technique. I want to re-iterate that you probably don’t want to use it in production right away, both due to still not perfect browser support, and the overall novelty: we don’t know which issues we can uncover with it.
On the other hand, this technique is so new, that we don’t know what it unlocks next. In the same way, space togglesGo to a sidenote did unlock cyclic toggles, and cyclic toggles now unlocked layered toggles, something else could be on the horizon, and we just need to continue trying to go beyond what, we think, is possible.
Published on with tags: #CSS Layers #CSS Variables #CSS Logic #Style Queries #Experiment #Practical #CSS