Back in July 2020, I got an email from James0x57 (I always try to refer to people by their name, but I think I get the sense they prefer to go by screen name) that says:
The entire world of branching conditional logic and bulk feature toggling for custom CSS properties is possible and only exists because of a tiny footnote in the CSS spec that has gone unnoticed.
That line is:
Note: While <declaration-value> must represent at least one token, that one token may be whitespace.
In other words, --foo: ;
is valid.
If you’re like me, this doesn’t read as some massive revelation that unlocks huge doors, but to smarter people, like James0x57, it does! We started working on a draft blog post, but for various reasons it didn’t make it all the way here. One of those reasons is that I just wasn’t getting it. Call me dense, sorry James0x57. One demo they sent me when I asked for a super dumbed-down example was helpful though, and I think it’s kind of clicked for me. Here’s my interpretation:
Let me attempt to explain:
- The breakpoint we’ve set up here is a 900px
max-width
media query. You can see that’s where the variable--mq-sm
flops frominitial
to an empty space value. - When the browser window is wider than 900px, that the value of
--mq-sm
isinitial
.- That makes the variable
--padding-when-small
contain two values —initial
and2rem
—which, I guess is invalid. - So when we actually set the padding and call that variable like
padding: var(--padding-when-small, var(--padding-when-large))
, the second value (the “fallback”) is used because the first value is invalid.
- That makes the variable
- When the browser window is narrower than 900px, the
--mq-sm
value is a space.- That makes the variable
--padding-when-small
value"(space)2rem"
which, I guess is valid. - That means when we actually set the padding and call that variable like
padding: var(--padding-when-small, var(--padding-when-large))
, the first value is used.
- That makes the variable
So, now we can flip the padding between two values by changing a placeholder variable.
That clicks for me.
When I see this as simply changing a single value, it’s almost like uh, ok, you’ve found a really complex way to change some padding, but you could have just changed the padding in the media query. But the trick is that now we have this placeholder variable that has changed and we can key into that to change unlimited other values.
We could have a single media query (or set of media queries) in our CSS that only toggles these placeholder variables and we use elsewhere to toggle values. That could be nice and clean compared to sprinkling media queries all over the CSS. It’s a proper toggle in CSS, like a form of IF/THEN logic that we haven’t quite had before.
James0x57 extended that thinking to all the logical possibilities, like AND, OR, XOR, NAND, NOR, and XNOR, but that lost me again. Not really a computer scientist over here. But you can follow their work if you want to see real world usage of this stuff.
This variable stuff is wild and gets very confusing. I noted in a possibly recent (but the byline says 2015?) article from Patrick Brosset that covers some tricky CSS custom properties stuff. For example, fallbacks can be infinitely nested, like:
color: var(--foo, var(--bar, var(--baz, var(--are, var(--you, var(--crazy)))));
Also, valid values for CSS custom properties can have commas in them like this:
content: var(--foo, one, two, three);
Is that really just one fallback with a single one, two, three
value? This is rather mind-bending.
Anyway, fast-forward a bunch of months now, and CSS trickery master Lea Verou has set her sights on this whitespace-in-custom-properties stuff:
What if I told you you could use a single property value to turn multiple different values on and off across multiple different properties and even across multiple CSS rules?
It’s the same trick! In Lea’s example, though, she uses this ability to:
- set variations on a button, and
- set four different properties rather than one.
This really hones in on why this is the concept is so cool.
Lea points to some downsides:
There is no way to say “the background should be red if
--foo
is set and white otherwise”. Some such conditionals can be emulated with clever use of appending, but not most.And of course there’s a certain readability issue:
--foo: ;
looks like a mistake and--foo: initial
looks pretty weird, unless you’re aware of this technique.
We’re certainly entering the next era of how custom properties are used. First, we used them like preprocessor variables. Then we started seeing more cascade and fallback usage. Next, we used it alongside JavaScript more frequently. Now this.
There is even more writing about keeping CSS preprocessor variables around, not so much for the times when you only need what they can do, but for the things that only they can do, like having their color values manipulated.
Hey, nice work!
But i cannot found a NOT in pure CSS for this technique…. AND and OR are nice and easy, but how to do a NOT?
There’s two ways. (as far as I’ve figured out, with what’s available today)
One is to toggle it with a paused keyframe animation – it’s not great but here’s a demo: https://codepen.io/james0x57/pen/mdExrXy
The other way is what I did in most of css-sweeper which is: When you set your initial state, also set a second variable to the logical inverse. From there, any time you need a NOT further in the logic, you have to mirror what you’re doing onto the NOT bit & carry it along with the primary bit. Basically it’s faking it with 2 bits representing different states of the same bit – like how DNA, quarks, magnets, etc all have mirrored pairs at all times. There’s a lot in this code but I can’t link to lines: https://raw.githubusercontent.com/propjockey/css-sweeper/master/index.html Here’s a screenshot of some of that setup: https://i.imgur.com/aho538M.png
Like this?
Ok … since the “boolen-like” values never pop out of a calc(), you can keep NOT values explicit. I get that.
The animation-technique is quite interesting!
Thanks for your answer!
if your CSS minifier changes
--mq-sm: ;
to--mq-sm:;
this trick breaks, right? seems to for me. i’d be very curious how consistently the behaviour of these exceptions are implemented across various browsers and survive various minifiers, etc.Yes, you have to avoid broken minifiers like CSSNano that’s built on PostCSS. In React that means including your CSS manually in the /public/index.html file: https://create-react-app.dev/docs/using-the-public-folder/
@jon_neal has been doing some awesome work writing CSS parsers that handle this (and MANY other problem areas with PostCSS) correctly so hopefully his work makes it into that ecosystem.
It’s a nice known trick to play with CSS variables in a way some state sets a variable to “unset”, therefor forcing the fallback value. I’ve played with this in the past. CSS has a lot of hidden surprises, just need to DIG ;)
I’ve invented a method which resembles the one in this article:
You should read the StackOverflow answer for more details.
(the second method is real fun)
What’s cool about this, is the ability KNOW when a certain numerical variable hits a certain target value and then control everything from. I came up with a way to create a CSS boolean which is actually useful.
Technique used it in my Pen: https://codepen.io/vsync/pen/mdEJMLv
I wanted a one-var-to-rule-them-all solution, so I’ve invented a nice little trick, which I actually thought of writing about here, but hadn’t gotten to it, because I am building something awesome in Codepen, for Codepen users! And while doing that, I carried away making two more cool things…
Will be ready this week.
You’ll love it.
An even simpler version: https://codepen.io/CarterLi/pen/xxOWWyX
@carter – it might be shorter but it’s missing the point that you want one-single boolean variable to control a multiple aspects of your CSS, therefore you need an actual single-variable
--bool
, or whatever name. You code only affects simple things (my demo also, but I thought it delivers the point, in a way that shows how calculating a 0/1 boolean can be used in many situation, which one of them is with keyframes).Imagine you have a 400 lines of CSS, and you incorporate this boolean variable in much of the code, in many ways. you wouldn’t want keyframes created for each and every situations. First you try everything you can without using the keyframes hack, and only if it’s impossible – utilize it as a last-resort, because it generates too much CSS and pollute the global env with named keyframes.
I suppose that
--mq-sm: ;
is a valid statement because UNTYPED css variables are lazily parsed.A value is valid as long as there are charactors to parse, including whitespaces. I think untyped css variables are pretty much like macros in C, they both have their meaning only when being used somewhere.
But
--mq-sm: initial
is an exception.--mq-sm: initial
behaves like--mq-sm
is not defined at all, instead of the valueinitial
.It seems that browser does recognize CSS keywords
initial
unset
inherit
when defining custom properties. The truth is ifinitial
is not recognized,--padding-when-small: var(--mq-sm) 2rem
should be parsed as---padding-when-small: initial 2rem
, whileinitial 2rem
is a perfect valid value of css properties.Another proof is that if we define
--mq-sm
something other thaninitial
, the example doesn’t work.So the truth is that
--mq-sm: initial
behaves like--mq-sm
is not defined, which makesvar(--mq-sm)
an invalid expression. Sincevar(--mq-sm)
an invalid,--padding-when-small: var(--mq-sm) 2rem;
is an invalid expression, which makes--padding-when-small
undefined.Thus
padding: var(--padding-when-small, var(--padding-when-large));
works.computedStyleMap and getComputedStyle for browsers having no typed cssom support are very good helpers for debugging css properties.
Another thing. If you were to combine this trick with animations? You could, in a manner of speaking, create a Switch-esque effect where properties start to change depending on the number assigned to a property.
See the Pen Using Space Toggles and Paused/Negative-Delayed Animations to create numerical options by Rock Starwind
(@RockStarwind) on CodePen.
! ^ This is next-level Space Toggle usage. They’ve essentially created an index-addressable array in pure CSS
Anyone wanting to do significantly advanced script-free CSS game programming (for the fun of a challenge) may want to learn what @RockStarwind has invented here!
It doesn’t work with Firefox. Apparently, pausing the animation means FF never assigns the initial values.
Lea Verou had a post a few weeks ago on this very thing.
https://lea.verou.me/2020/10/the-var-space-hack-to-toggle-multiple-values-with-one-custom-property/
She sure does, that’s why I talked about it in the article and included her demo and quotes from her.
Hi Chris Coyier.
Thanks for the inspiring article.
You misunderstand
--foo: initial;
.The keyword
initial
sets, also in this case, the property to its initial value.The W3C says:
Carter Li has already described the effect of
--mq-sm: initial;
:Pretty clever. But this being a hack, it’s actually rather confusing to read, which in big projects can become a nightmare to debug/maintain. Readability should be the #1 priority in projects designed for the long term. Maybe pre/post processors can take advantage of this in a more elegant way?
I’m fascinated by this, but, I’ve read the article several times and, so far, whatever I try with the Space Toggle, I am unable to achieve any effect which I can’t already achieve more simply with @media queries.
Following the padding example at the top, here’s what I came up with:
and… this works.
But it’s exactly the same as this:
Now, obviously I’ve missed something (and no doubt it will seem obvious once I finally grasp it) but what is it exactly that the Space Toggle enables that isn’t just a clever way to insert @media query values into non-media-query-contained style declarations?
If you’re looking for something that is going to totally re-invent how CSS works forever, I don’t think you’re gonna find it here. It’s a (wait for it) CSS trick.
But I do think it’s very neat to be able to set multiple properties when one custom property changes. Sure, that could be media-query-esque, but it could be something else like the fact that JavaScript changes it, or a
:checked
state change.Would be extra neat to have a way to do it without it feeling like a hack.
With an extra variable, you can do this. For example have a
--dark
and--light
switch where one isinitial
and the other one a space. Then usebackground-color: var(--light, white) var(--dark, black);
.One will resolve to its fallback, the other one will resolve to space and thus is ignored. See the pen below.