Conditionals on Custom Properties
Saw this from Lea being passed around yesterday:
A historical day for CSS 😀🎉
— Lea Verou (@LeaVerou) June 13, 2024
If you write any components used and/or styled by others, you know how huge this is!
background: if(style(–variant: success), var(–green));
Even if you don’t, this will allow things like:
padding: if(var(–2xl), 1em, var(–xl) or var(–m),… pic.twitter.com/cXeqwBuXvK
Whoa, right?! Seeing an :if()
function in CSS gets my mind going in all sorts of directions. But what Lea is referring to is a GitHub issue she opened in the CSS Values Module Level 5 repo. I thought I’d take some notes on that discussion as I try to wrap my mind around what’s going on.
This is about conditional statements, but mostly custom properties
The ticket describes a long-running desire for developers to apply style if()
a certain condition is true. There are already some workarounds that get us close:
- Using custom properties as a Boolean to apply styles or not depending on whether it is equal to
0
or1
. (Ana has a wonderful article on this.) - Using a placeholder custom property with an empty value that’s set when another custom property is set, i.e. “the custom property toggle trick” as Chris explains it.
- Container Style Queries! The problem (besides lack of implementation) is that containers only apply styles to their descendants, i.e., they cannot apply styles to themselves when they meet a certain condition, only its contents.
Syntax
Lea isn’t proposing a single, particular direction but rather is poking at what might the minimum viable approach to get us to conditional statements, one that can be extended later as needs and ideas arise.
That said, here’s one possible way to define what if()
might look like:
<if()> = if( <container-query>, [<declaration-value>]{1, 2} )
…where:
- Values can be nested to produce multiple branches
- If a third argument is not provided, it becomes equivalent to an empty token stream
As I best understand it — which may be very little — we identify a container query (just style()
, I think?) the declared style condition we want to match and set style with one of two declared styles, one as the default style, and one as the updated style when a match occurs.
.element {
background-color:
if(style(--variant: success),
var(--color-green-50), /* Matched condition */
var(--color-blue-50); /* Default style */
);
}
The default style would be optional, so I think it can be omitted in some cases:
.element {
background-color:
if(style(--variant: success),
var(--color-green-50) /* Matched condition */
);
}
Nesting conditions
Lea offers an example of how a nested set of conditions might work as a third possible argument:
background-color: if(
style(--variant: success), var(--color-success-60),
if(style(--variant: warning), var(--color-warning-60),
if(style(--variant: danger), var(--color-danger-60),
if(style(--variant: primary), var(--color-primary)
)
),
)
);
…but goes on to suggest a syntax that would make this less verbose and meta.
<if()> = if(
[ <container-query>, [<declaration-value>]{2} ]#{0, },
<container-query>, [<declaration-value>]{1, 2}
)
In other words, nested conditions are much more flat as they can be declared outside of the initial condition. Same concept as before, but different syntax:
background-color: if(
style(--variant: success), var(--color-success-60),
style(--variant: warning), var(--color-warning-60),
style(--variant: danger), var(--color-danger-60),
style(--variant: primary), var(--color-primary)
);
What about other types of checks?
I love this question Lea poses:
How hard would it be to support other queries beyond
style()
? E.g. size queries,supports()
,media()
? (style()
does address the vast majority of use cases, but if any of these are easy, we may as well support them too.)
So:
background-color: if(
supports( /* etc. */ ),
@media( /* etc. */ )
);
Neat-o idea! The challenge would be container size queries. As currently specced and implemented, there is no size()
function; instead it’s more like an anonymous function:
@container (width > 65ch) { /* Style rulesets */ }
I’ve tried to understand the limitations of size queries before, but admit it’s just a lot for this blondie to grok. Whatever the case, @andruud has a succinct answer:
I don’t see why we couldn’t do
supports()
andmedia()
, but size queries would cause cycles with layout that are hard/impossible to even detect. (That’s why we needed the restrictions we currently have for size CQs in the first place.
Commas, colons, semicolons, or question marks?
After the Working Group discussed the proposal in a meeting, more was raised on the specifics of the syntax, namely what characters make the most sense and for the most readable code.
True to form, Tab Atkins sums it up nicely:
if(style(...): foo, bar)
could be a single valuefoo, bar
(and IACVT if false), or two values (foo
if true,bar
if false). This does have a defined parse – it’s always two values. But if you wanted it to be one value, and then IACVT for false, you can’t write that.(This ambiguity doesn’t occur if there are multiple chained conditions.
if(style(...): foo; style(...): bar, baz)
unambiguously doesn’t have a default case; thebar, baz
is the whole value for the second condition. It’s only in the single-condition case that we haven’t yet gotten unambiguous information about what the separator is.)
First thing to note there is the addition of a colon following the style()
function in place of the comma that was initially proposed:
background:
if(
style(...): /* instead of `style(...),` */
foo,
bar
)
The second thing to note is a possible issue with using commas to separate declaration values:
if(
style(...): foo, bar
)
That could confuse what we mean. Is it that foo
and bar
are separate values, where one is the default style and the other is the updated style? Or are we defining one compound value, foo, bar
that is applied when the condition is false? In other words, can we omit a default value if needed and still have the condition properly evaluated?
Tab suggests a way to say “no default condition” in the syntax, e.g.:
if(
style(...): foo, bar;
no-value
)
Lea suggests we might be able to use a different operator to separate the style condition from the declaration values, such as ?
or :
so that it’s possible to make the last argument mandatory while accepting an empty value there:
if(style(...) ? foo)
If not that, then maybe a keyword to denote no default as the last argument:
if(style(...) ? foo, no-value)
So, where’s this at?
Lea’s X post provides this example:
background: if(style(--variant: success), var(--green));
As well as this one to illustrate (I’m nesting it to help me read it):
padding: if(
var(--2xl),
1em,
var(--xl) or
var(--m),
.5em
);
Followed, of course, by a screenshot that the Working Group resolved to add the if()
function to the specification.
There seems to be a lot more nuance than what I’m capturing here, but these are just my notes for general understanding and keep an eye on how it all shakes out.
Likes
Reposts