Open LeaVerou opened 5 months ago
And in #9539 we already agreed on a way to separate such things: a semicolon, which can be replaced with an "implicitly upgradeable" comma in specific cases.
I was not familiar with that issue, or its resolution. Now that I've read it, I think we should go for property: if(<condition>; <value-if-true>; <value-if-false>)
. Or, if the resolution of #9539 is open to dispute, I really like the idea of plain parentheses to distinguish values (regardless of any commas they contain), so it could be property: if(<condition>, <value-without-commas-if-true>, <value-without-commas-if-false>)
or property: if(<condition>, (<value-with-commas-if-true>), (<value-with-commas-if-false>))
. Or maybe plain parentheses can include ternaries as well, in which case it could be property: (<condition> ? <value-if-true> : <value-if-false>)
, where the values can contain commas.
I don't want that either. But we can use
:
here, which is consistent with how we relate values to properties in declarations.if(cond1: val1, cond2: val2, fallback) /* only if no value contains commas */ if(cond1: val1; cond2: val2; fallback) /* general syntax */
I feel like it would be more readable to just have one condition in if()
:
if(cond1; val1; if(cond2; val2; fallback))
Ah, we are now in full-blown bikeshedding mode 😅
:
is valid in custom properties, so it seems unnecessary limiting to disallow it in conditionals that may be used in said custom properties.
We could also disallow it, like we did with braces when we needed to for Nesting (I think? Or did we disallow --tagname
instead?).
How about both? If we can have commas that become semicolons when needed, we could also have colons that become semicolons when needed.
I think it’s a big antipattern to use ,
or ;
to separate some arguments and another character to separate others. Either we use two distinct characters, or we use ;
/,
for both.
@benface Even if if()
does accept multiple conditions, authors can still choose to nest it instead. So if you find that more readable, you can do that! Personally, I don’t, and nested parens are easy to get wrong.
@benface Yes, if we require additional conditions to be nested then we can make do with just ,
or ;
.
But note when there are lots of possibilities it can get cumbersome with so much nesting. Though I still prefer that over if (cond ? iftrue : iffalse)
@benface Even if
if()
does accept multiple conditions, authors can still choose to nest it instead. So if you find that more readable, you can do that! Personally, I don’t, and nested parens are easy to get wrong.
How would if()
accept multiple conditions without using :
after the condition?
EDIT: Ah, I guess what someone wrote above:
* {
--prop: if(style(--x: 'one') ? 1 : style(--x: 'two') ? 2 : 3);
}
The more I think about it, the more I feel like if(cond ? iftrue : iffalse)
is wrong. I really like the ternary syntax, but I don't like that it's a special syntax that only works in a specific function. So if we can't make it work everywhere (i.e. with plain parens), I prefer the ,
-or-;
approach (and @Loirooriol's proposal for multiple conditions is also growing on me).
For me, the ?
and :
in if (a ? b : c)
are preferable, as this will allow us to later use them in calc()
and other similar cases without the wrapping if()
.
Yes, we could have the if()
with a different syntax while still adding the ternary to calc()
later, but why if we can reuse the same syntax inside it?
I think we should go with if ([cond ? iftrue : iffalse ]+)
I feel strongly that we should not mint a new separator that indicates the end of a <decl-value>
. Doing so means there's one more character we can't use for properties in the future, or that we then have to invent another convention for allowing values with such a character to be given in if()
(and anything else using this syntax convention).
We should stick with comma and upgradeable semicolon. Comma is ubiquitous, and semicolon is already excluded from <decl-value>
so it's safe forever. This then means that the function is a comma (/semicolon)-separated list of condition/value pairs, which is understandable and easy to understand in the existing mental model of CSS syntax.
(This objection does not apply to places where the grammar already distinguishes the character from the surrounding values, like whatever we use between the condition and the value. So using ?
is acceptable there from a syntax-design standpoint, tho I'd prefer :
as it more closely matches several vaguely-related syntax conventions, like CSS properties, JSON properties, Python if
statements, the upcoming JS match()
expression, etc. The only syntax convention already using ?
is the JS/etc inline conditional, which pushes us to use :
to separate the "true" value from the rest of the condition.)
Separately, while I like inline conditionals in JS, I think it's fairly common wisdom that stacked inline conditions (a ? b : c ? d : e ...
) is moderately unreadable and often linted against. Unless you carefully format it, it's very hard to follow the logic, and the operator precedence isn't immediately clear either. I don't think we want to lean on that convention for multi-condition if()
.
To be more productive, then, my suggestion for the syntax is:
if( [<<if-cond>> ':' <<decl-value>> ]# )
<if-cond> = <<boolean>> | else
With the condition that else
is only valid in the final pair. (And if there's no else
and all the conditions fail, the value is IACVT.)
To be more productive, then, my suggestion for the syntax is:
if( [<<if-cond>> ':' <<decl-value>> ]# ) <if-cond> = <<boolean>> | else
With the condition that
else
is only valid in the final pair. (And if there's noelse
and all the conditions fail, the value is IACVT.)
So something like this?
property: if(style(--condition: true): var(--value-if-true), else: var(--value-if-false));
Makes a lot of sense to me!
With the condition that
else
is only valid in the final pair. (And if there's noelse
and all the conditions fail, the value is IACVT.)
Syntax aside, we need to decide separately if a lack of else means IACVT or an empty token stream. I think the latter may be more composable.
With the condition that
else
is only valid in the final pair. (And if there's noelse
and all the conditions fail, the value is IACVT.)Syntax aside, we need to decide separately if a lack of else means IACVT or an empty token stream. I think the latter may be more composable.
My vote would be for IACVT + a separate mechanism for reverting IACVT declarations, like the one you proposed in #10443.
That is orthogonal, since even if it returns an empty token stream it can make the resulting property IACVT. E.g. this would work fine with the empty token stream behavior:
border-radius: if (style(--pill) ? calc(infinity) ) !revertable;
But it means something like this is possible too, without having to explicitly specify an empty fallback:
background: if (style(--skeuomorphic) ? var(--texture)) var(--color) ;
@tabatkins
With the condition that
else
is only valid in the final pair.
If the semantics are first true condition wins (which is the easiest to implement and understand, especially) and else
is defined to always match then it's valid everywhere but probably only means what you want when it's at the end. This allows some printf debugging by sticking an else
in the middle, and it's easy enough for linters to detect and warn about a medial else.
Alternately you could define it as matching whenever nothing else matches so that it's valid anywhere (but only once). I don't think this buys much over the simpler rule.
@kizu I don't understand why it would be good to allow if(x?y:z)
and calc(x?y:z)
. That would make if
by itself fairly useless once the latter lands. Why not skip straight to calc(x?y:z)
? [I much prefer the switch style to the ternary but note that both could exist if the ternary is just a regular calc expression]
Why not skip straight to
calc(x?y:z)
?
Because people desire to use conditionals outside of calculations.
Syntax aside, we need to decide separately if a lack of else means IACVT or an empty token stream.
Empty token stream can be accomplished by just omitting the value part, so I don't think it needs to be specially addressed.
If the semantics are first true condition wins (which is the easiest to implement and understand, especially) and else is defined to always match then it's valid everywhere but probably only means what you want when it's at the end. This allows some printf debugging by sticking an else in the middle, and it's easy enough for linters to detect and warn about a medial else.
Ah, true, I do find that valuable (and end up making this same mistake every time I design something like this, lol).
And yes, it would be "first true condition wins" semantics, absolutely. Nothing else is reasonable, imo.
Good to see progress. 🚲🏚️ Given the extended syntax, is this an "if()" or a "match()"?
A random thought after reading https://github.com/w3c/csswg-drafts/issues/10489, which proposes to align mix()
to have of
and to
keywords instead of commas with what is defined for progress()
which has from
and to
.
I guess, another option for if()
could be something like if(A then B else C)
, although it might be a bit weird when the commas are involved, as visually you'd think the keywords belong to different parts: if(A then B, C else D)
.
(not sure how I feel about this option compared to others, but I felt it might be useful to bring it up here as it was not mentioned yet)
then
and/or else
could be valid parts of A
, B
or C
.
Maybe a grid template area named then
:)
AFAIK there is no way to have a new separator for this case that isn't prone to the exact issue that ;
is designed to resolve.
Regardless of syntax ambiguities, conditionals need to be general purpose. In particular, people should be able to use keywords inside them. So special-casing some keywords as separators seems a very bad idea. We already made this mistake in grid (see #1137), it shouldn't happen again.
@jimmyfrasche
If the semantics are first true condition wins (which is the easiest to implement and understand, especially) and else is defined to always match then it's valid everywhere but probably only means what you want when it's at the end. This allows some printf debugging by sticking an else in the middle, and it's easy enough for linters to detect and warn about a medial else.
This has the same issue as many other proposed separators, now else:
is invalid in other properties (including custom properties).
But yes, +1 for the "first true wins".
@tabatkins
Empty token stream can be accomplished by just omitting the value part, so I don't think it needs to be specially addressed.
I don't understand what you mean. Omit what, how? Can you write an example?
@martinthomson
Good to see progress. 🚲🏚️ Given the extended syntax, is this an "if()" or a "match()"?
It’s still a conditional, so still if()
. match()
would be incredibly confusing.
Agreed with @Loirooriol wrt keywords, but also this is one of these few cases where conciseness is extremely important because this will be used all over the place. So can we please stop suggesting lengthy function names and separators?
The ternary syntax makes the most sense to me, I'd imagine in most places a class would be used rather than an if
, though one place where if
does excite me is with functions and mixins.
/* Syntax not finalised */
@function --lighten-darken(--base, --percent, --ld: 'lighten') {
@return color-mix(in srgb, var(--base), if(style(--ld: 'lighten') ? #fff : #000) var(--percent));
}
.some-class {
color: --lighten-darken(var(--palette-primary), 30%, 'darken');
}
.some-other-class {
background-color: --lighten-darken(var(--palette-secondary), 60%, 'lighten');
}
I love the idea of having multiple statements and the first true being applied though it really feels more like a switch
than an if
.
background-color: switch(
matches(:disabled): var(--color-disabled),
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),
default: var(--color-fallback)
);
EDIT: add matches
selector and default
to switch.
Is there a mechanism for greater than and less than?
border-radius: if(compare-style(--some-enum >= 2) ? 4px : 0px);
I see a lot of people bringing up the question of whether this is more like an if
, switch
or match
.
Just my two cents, but these days, every language that's ever had an if-statement eventually realizes they want is pattern matching - an if
statement generally implies branching, but CSS is declarative, so a match
might be a better choice.
Something like @Link2Twenty 's switch
statement (above) but allowing expressions with comparison operators.
This would be much more readable than the ternary style syntax inside the if(...) - especially given that nesting ternaries is already frowned upon in other languages.
It also solves the ambiguity problems and the case where you don't want a default - which, in addition, you make more readable, safer, and easier to parse by actually requiring a default:
in the match/switch to indicate a fallback value when no condition is matched.
@LeaVerou I opened #10593 and #10594 per my earlier comment re a matches()
function. If they were to be accepted, a math-based switch example like the one below could be rewritten inline using reusable and type-safe boolean logic.
BEFORE | AFTER |
---|---|
```postcss label { --selected: 0; &:has(:checked) { --selected: 1; } grid-template-columns: auto calc(var(--selected) * 20px); svg { opacity: var(--selected); } } ``` | ```postcss label { --selected: match(:has(:checked)); grid-template-columns: auto if(var(--selected) ? 20px : 0); svg { opacity: if(var(--selected) ? 1 : 0); } } ``` |
** This is a simpler example, and personally, I might still opt for a math-based switch like this in a similar situation, but in many situations, boolean logic might be easier for a developer to grok at first glance.
This has the same issue as many other proposed separators, now else: is invalid in other properties (including custom properties).
No, it's fine as long as it sits in the condition position of the grammar. It's just a special condition, spelled else
, that always matches. It doesn't interact with the grammar of the value part at all. if(foo(): first value, else: second value)
I don't understand what you mean. Omit what, how? Can you write an example?
If you don't want a value, you can just not write a value, like if(foo(): , else: a value)
. That's first value part is perfectly reasonable, it's just empty.
So can we please stop suggesting lengthy function names and separators?
Right, thus if( [ <condition> ':' <decl-value>? ]# )
. Don't think it's possible to get any shorter/simpler.
To be more productive, then, my suggestion for the syntax is:
if( [<<if-cond>> ':' <<decl-value>> ]# ) <if-cond> = <<boolean>> | else
With the condition that
else
is only valid in the final pair. (And if there's noelse
and all the conditions fail, the value is IACVT.)
@tabatkins If we settle on the if( [<<if-cond>> ':' <<decl-value>> ]# )
grammar, how would values that contain commas be handled? Would the parser not consider a comma ,
the statement terminator unless preceding a value followed by a colon :
?
Consider this example:
* {
--val: if(media(width > 200px): media(width > 100px), media(width > 50px), media(width > 10px): 10px),
}
While it may be a bit convoluted, this would be the expected condition-result map, as I see it:
Condition | Result | Result type |
---|---|---|
media(width > 200px) |
media(width > 100px) |
"<boolean>" (re #10593) |
media(width > 50px) |
10px |
"<length>" |
(omitted else ) |
IACVT |
This is part of why I found the ternary syntax to be a bit clearer, where neither ?
or :
are currently used in CSS to delimit any values iirc.
how would values that contain commas be handled?
We would rely on the semicolon-upgrading text.
So your example would be grammatically invalid as written, but you could upgrade the argument-separating commas to semicolons, like:
if(media(width > 200px): media(width > 100px), media(width > 50px); media(width > 10px): 10px)
So the first value is media(width > 100px), media(width > 50px)
and the second value is 10px
.
(I presume that was your intended reading; if not, I'm not sure what you meant it to parse as.)
Nice brainstorming so far everyone, but it's time to narrow down the design space so we can make progress. I think we should break down the resolution in two steps:
if( [ <boolean> <c1> <decl-value> <c2>]+ <decl-value> )
where <c1>
and <c2>
are some single character tokens. This is something that will be used all over CSS so conciseness is important.<c1>
, ?
, :
, or ,
/;
<c2>
, :
, or ,
/;
I feel like using ,
introduces (potential) confusion for any kind of list, like font-family/box-shadow/background/etc. unless you tuck it away in a variable, while ;
and :
are already significant symbols and might trip up parsing (emphasis on "I feel" and "might" because I don't work on browser engines!).
The question mark not only feels semantically correct (with the sentence-like structure of is cond true ? if so, here's A : if not, here's B
), but if paired with :
, we get the familiarity of ternaries many already know from in other languages like JS and PHP (see example one
). That said, if the question mark should be reserved for a higher purpose, it might be possible to go all in with :
(see example two
).
Would the below be viable examples?
one {
font-size: if(media(width < 800px) ? 1rem : 1.5rem);
font-family: if(media(print) ? Times New Roman, serif : Helvetica, sans-serif);
color: if(
style(--accent: blue) ? cyan :
style(--accent: red) ? pink :
white
);
}
two {
font-size: if(media(width < 800px) : 1rem : 1.5rem);
font-family: if(media(print) : Times New Roman, serif : Helvetica, sans-serif);
color: if(
style(--accent: blue) : cyan :
style(--accent: red) : pink :
white
);
}
I would learn towards the ?:
combo due to ternary familiarity, and how it's easier to pick up where the conditions are, at a glance. It's terse, but readable, whereas the abundance of :
in the other example can be confusing unless formatted like above. If you gave me the following (contrived) example, I'd be confused as to where one thing ends and the other begins:
color: if(style(--accent: blue) : var(--color-main) : style(--accent: red) : oklch(from var(--color-secondary) 0.85 50% calc(h/2)) : var(--color-base))
Anyway, just my two cents, and I wanted to make sure I understood what is potentially feasible. Very excited for this to land either way!
The syntaxes proposed and discussed here all echo the most criticized "bad practice" patterns in other languages - in my opinion, we need to think harder about the syntax and come up with something that doesn't do that.
If we push for something like this out of impatience, there's a good chance we're making the situation irreparably worse, in the sense that any future syntax attempting to address the problems and shortcomings of an early syntax will need to be backwards compatible, which really narrows our options - which are already narrow.
That said, why are we trying to force this into property-level conditionals?
Wouldn't it make more sense to take cues from established and popular languages such as SASS, where conditionals are structural concepts? CSS is really all about thinking and designing in terms of structure.
Something in this direction would be immediately easy for almost anyone to understand:
.element {
/* Base styles */
font-family: sans-serif;
color: black;
/* Conditional block */
@if (--theme == 'dark') {
color: white;
background-color: #333;
} @else if (--theme == 'light') {
color: #333;
background-color: #f0f0f0;
} @else {
/* Default theme */
color: #444;
background-color: #e0e0e0;
}
/* Media query integration */
@if (media(min-width: 768px)) {
width: 750px;
} @else {
width: 100%;
}
/* Nested conditions */
@if (--layout == 'grid') {
display: grid;
@if (media(min-width: 992px)) {
grid-template-columns: repeat(3, 1fr);
} @else if (media(min-width: 768px)) {
grid-template-columns: repeat(2, 1fr);
} @else {
grid-template-columns: 1fr;
}
}
}
It's a bit more verbose, but a lot more readable and less cramped than trying to force complex expressions to fit into property values, I think? It also accounts better for situations where you want the same condition to control two alternative properties - that's already common with things like media queries, and I'm betting you would find, in practice, the same requirement would exist here.
I understand there is a potential for creating cycles with a feature like this, but... developers should know not to do that. 😅
And it's not too difficult to think of a relatively simple approach to mitigate that problem, without resorting to complex static analysis or ugly runtime checks like cycle prevention - for example, maybe we specify a simple two-phase evaluation, such that:
So to illustrate, consider an example like this:
:root {
--theme: 'light';
}
.element {
@if (--theme == 'light') {
background-color: #333;
--theme: 'dark';
} @else {
background-color: #fff;
--theme: 'light';
}
}
In terms of execution, it would work something like this:
.element {
/* PHASE 1: Condition evaluation */
@if (--theme == 'light') {
background-color: #333;
} @else {
background-color: #fff;
}
/* PHASE 2: Property modifications */
@if (--theme == 'light') {
--theme: 'dark';
} @else {
--theme: 'light';
}
}
In other words, you can update custom properties, but only for the purposes of using those values in CSS properties - it's not going to cascade or have any impact on other conditionals.
If users do attempt to write CSS with cascading conditional effects, in addition, perhaps the browser would issue a warning, letting them know they're getting a previous custom property value in their conditionals, rather than the updated value.
Full disclosure: I'm not at all certain about this idea, just putting it out there. 😅
It feels like the @if
you're describing is a way to replace all conditional rules with a new syntax that can mix and match them.
.element {
@if media(width >= 993px) and supports(display: grid) and container(orientation: landscape) and style(--accent-color: blue) {
content: 'You match our exact use case';
} @else {
content: 'Oh no!'
}
}
I have to admit @if
does feel to fit the current CSS syntax better than inline but ?
, :
would get my vote for inline syntax.
The downside of the publicity this issue got is many people are parachuting into it and confidently asserting they can solve all the problems without being aware of the numerous constraints we need to navigate for something like this to even be possible. 😕
@mindplay-dk @Link2Twenty Believe me, we’d all LOVE to do @if
and do it properly, and me more than anyone. However, there are currently technical constraints that prevent us from doing so, namely that we don’t want a rule that would have different behavior on descendants (like style queries) and different on the element itself (IACVT). Perhaps in the future it may become possible, if we implement something like #10443 to make IACVT behave as similarly to the regular cascade as possible.
for example, maybe we specify a simple two-phase evaluation, such that:
- CSS properties are updated in the first phase, and
- custom properties are updated in a second phase.
These are not independent, custom properties can depend on certain CSS properties, and CSS properties often depend on custom properties. Also, you’re proposing a very major change to how CSS is evaluated, and major changes require major benefit to be justifiable, otherwise they stand no chance of implementation.
I understand there is a potential for creating cycles with a feature like this, but... developers should know not to do that. 😅
Yeah, "users should just not do that" is not how you design a language (or any UI really).
Wouldn't it make more sense to take cues from established and popular languages such as SASS, where conditionals are structural concepts?
Even when languages have block conditionals, they ALL also have some kind of inline conditional, because there are just so many use cases where only a small part of a value actually varies, and a block conditional would actually make code less readable. Yes, even Sass. 🙂
CSS is really all about thinking and designing in terms of structure.
Is it now? 😅
The downside of the publicity this issue got is many people are parachuting into it and confidently asserting they can solve all the problems
I'm sure you're not talking about me, because I wrote in italics "I'm not sure about this idea".
I've been watching this thread for about a month, so I don't think I'm parachuting in either.
I don't know how CSS engines are implemented internally, and my idea might be totally unrealistic. I'm not a browser or language engineer, I'm only commenting as someone who has been writing CSS since the 90s.
I'm just an end user, but you seem to agree that my opinion probably echoes that of most end users:
Believe me, we’d all LOVE to do
@if
and do it properly
That's why, as said, I hope we don't push for this out of impatience - any web feature effectively becomes a permanent fixture of the platform for eternity. If we add a half feature with already well established drawbacks, the syntax and complexity better not get in the way of doing something different, in the future, that "we'd all LOVE", right? 🙂
As someone who's off-and-on followed a few of the parent issues for years (I didn't even know this aggregate issue existed until today), I would like to point out that @if
could be desugared into a series of if(cond, then, else)
if needed.
Personally, I'd like to see both an at-rule block and a ternary expression happen, not just one or the other. Both are useful in their own right, and most major languages nowadays that have separate statement and expression contexts have both. It doesn't have to be either-or.
@dead-claudia I completely agree - but if I had to pick one, I wouldn't pick ternaries, because nesting them gets really confusing, really fast. There are good reasons why nested ternaries are frowned upon in other languages. If that's the only choice, confusing code and frowny faces is what we'll get.
I'd rather have high verbosity readable code than low verbosity unreadable code.
Unreadable code leads to bugs - verbosity only hurts developer sensitivities. 🙂
Quick question about the original comment: shouldn't the BNF for the multi-condition <if()>
be this?
<if()> = if(
[ <container-query>, <declaration-value>? ]#{1, }
[ , <declaration-value>? ]?
)
That 1. fits your intent and 2. brings consistency with var(...)
.
We are planning a prototype of this in Blink, and it would be great to see some progress here.
As far as I can tell from reading the comments, there are two main syntax proposals:
A) Comma/else, from @tabatkins / @Loirooriol:
background: if(
style(--status:error): red,
style(--status:warning): yellow,
else: green
);
else
is a special condition that always evaluates to 'true'.else:
is optional (i.e. may be empty).background: if(
style(--status:error): url(error.png), red;
style(--status:warning): yellow;
else: green
);
I see earlier proposals from Oriol have a plain <declaration-value>
as the fallback (no else:
), but I assume this isn't an option, since we couldn't distinguish a single fallback <declaration-value>
from a <cond> ':' <declaration-value>
. (See also #10818).
B) ?:, from @LeaVerou:
background: if(
style(--status:error) ? red:
style(--status:warning) ? yellow:
green
);
background: if(
style(--status:error) ? url(error.png), red:
style(--status:warning) ? yellow:
green
);
I can live with either, but the fact that <declaration-value>
allows :
does weigh in favor of (A) in my opinion.
@andruud Exciting that a prototype is in the works! I think we're at the point where we need separate issues to discuss the individual design decisions. I'll try to do that in the next few days.
FWIW I think my biggest issue with (A) is the verbosity of an explicit else:
clause — this is a feature that will be used all over the place, and thus is one of the few cases where terseness should be a priority.
Btw IACVT handling is identical across the two — unless I’ve misunderstood a part of Tab's proposal, their differences are purely syntactic.
we couldn't distinguish a single fallback
<declaration-value>
from a<cond> ':' <declaration-value>
I think we could distinguish them because only the last item of the comma/semicolon-separated list would use the former. All the other items in the list would need to parse <cond> ':'
before the <declaration-value>
. But I don't really mind having the else:
or not.
@mindplay-dk @Link2Twenty Believe me, we’d all LOVE to do
@if
and do it properly, and me more than anyone. However, there are currently technical constraints that prevent us from doing so, ...
I wonder why you (or anyone else in this thread) didn't mention it so far, though there are already general conditional at-rules named @when
and @else
in CSS Conditional 5.
So far, they only cover @supports
and @media
use cases, though. I.e. they do not cover (custom) properties at the moment. This is due to the technical constraints mentioned.
And yes, an inline solution and a structural solution don't exclude each other, though this proposal explicitly discusses an inline conditional solution. Any structural solution should be discussed separately.
I'd just note that both should be aligned with each other, i.e. use the same keyword (so probably when()
) and functions (e.g. style()
as initially suggested plus media()
and supports()
and possibly others).
Sebastian
Btw IACVT handling is identical across the two — unless I’ve misunderstood a part of Tab's proposal, their differences are purely syntactic.
@LeaVerou Oh, does that mean the :
would actually be optional in the ?:
approach? E.g.:
background: if(
style(--status:error) ? red:
style(--status:warning) ? yellow
);
(IACVT for --status:ok
, for example.)
FWIW I think my biggest issue with (A) is the verbosity of an explicit
else:
clause — this is a feature that will be used all over the place, and thus is one of the few cases where terseness should be a priority.
I do see that for short one-liners, e.g.:
width: if(style(--wide): 200px, else: 100px);
/* vs */
width: if(style(--wide) ? 200px : 100px);
For a multi-line thing with multiple clauses, though, the else:
seems to fit in a little better without feeling verbose. Could we somehow drop the else:
, and make it just: if(style(--wide): 200px, 100px)
? (Oriol seems to think "yes").
we couldn't distinguish a single fallback
<declaration-value>
from a<cond> ':' <declaration-value>
I think we could distinguish them because only the last item of the comma/semicolon-separated list would use the former. All the other items in the list would need to parse
<cond> ':'
before the<declaration-value>
. But I don't really mind having theelse:
or not.
@Loirooriol This would have to exclude :
from the fallback's <declaration-value>
, then? Otherwise, all of style(--status:warning): yellow
(a valid <declaration-value>
) would actually be treated as the fallback value?
background: if(
style(--status:error): red,
style(--status:warning): yellow
);
Maybe we can make the else:
part optional:
<cond> ':' <declaration-value>
. If that succeeds, it's a clause.<declaration-value>
. If that succeeds, it's fallback.In the hopefully super-rare event that you want literally style(--status:warning): yellow
(or similar) as your fallback value, you'd then do:
--foo: if(
style(--status:error): red,
else: style(--status:warning): yellow
);
You'd also still use else:
to get an empty (but valid) value, if needed.
This would make the above mentioned one-liner just width: if(style(--wide): 200px, 100px)
, beating the ?:
approach by two characters when you adhere to my whitespace preferences. :-)
Ah, I was thinking that the fallback wouldn't be optional. But sure, I guess IACVT if there is no fallback can be useful.
Your idea of an optional else:
seems to work too.
The problem with omitting else
(or something similar) is grammatical, and fundamental.
<declaration-value>
is really wide syntactically, so you can't distinguish it from other grammars. It needs an unambiguous marker preceding it to indicate "okay now it's time for decl-value".In theory, we could address the first by the presence/absence of the argument separator. That is, if(foo(): bar)
and if(foo(): bar,)
could indicate the two states - the first is IACVT if foo()
doesn't match, the second has an empty catch-all if foo()
doens't match. But that's a tiny, difficult-to-distinguish syntax difference, absolutely a language-design no-no almost all of the time.
The second, tho, is unresolvable. In if(foo(): bar, baz(): qux)
, baz(): qux
is a perfectly valid <declaration-value>
! So it's impossible to tell whether it was meant to be "two clauses with no catch-all, evaluating to either bar
, qux
, or IACVT" or "one clause with a catch-all, evaluating to either foo
or baz(): qux
".
Without an explicit and required start-of-clause syntax before the <declaration-value>
, it's impossible to separate these two cases. A <declaration-value>
simply cannot be directly preceded by a repetition or an optional value, ever, unless you explicitly specify that it's not allowed to match the grammar it's ambiguous with.
So, we either need to remove the grammar, or require a prefix. Removing the grammar requires that we either carefully carve out the existing syntax only (which makes it potentially dangerous to evolve things in the future, or handle errors), or carve out a wide syntax (like, basically banning <general-enclosed>:
from the start) and that still somewhat limits our future evolution potential. Requiring a prefix has no such downsides; the prefix doesn't have to be spelled else
, but if we don't use that, we'd be fighting against the headwind of nearly every conditional syntax in the world.
While writing this comment, I realized that neither syntax can handle "single condition with a comma in its value, and no catch-all" well. That is, if you write if(foo(): bar, baz)
it can't be interpreted as having a single condition with the value bar, baz
; under the else:
grammar it's just invalid syntax (baz
isn't a valid condition/value pair), while under the implicit catch-all it's a value of foo
and a catch-all value of bar
. In both cases, you need to supply a second always-false condition to give yourself the opportunity to put a semicolon in and force the comma to be part of the value.
We should fix this with another special clause, I guess - say, just else none
. Also a catch-all, but, resolves to IACVT, so you could write if(foo(): bar, baz; else none)
. You could do this in other cases, to make it clear that you know your conditions aren't exhaustive and might not match, but normally you'd just say nothing and let it implicitly trigger IACVT.
(Note the lack of a :
in the else none
; required to make it unambiguous.)
@tabatkins these are all non-problems in the if(?:)
syntax, so I'd argue you just listed some cons of the other syntax. 😁 Can we simply disallow :
from <declaration-value>
? Do we have any declarations where that’s valid? For custom properties, I can ask the HTTPArchive folks for some data.
Using ?:
doesn't change anything about my critiques. All that ?:
does it change the separators (from :,
or :;
), but once that swap is made, the problems are exactly the same - you'd still be distinguishing between if(foo()? bar)
and if(foo()? bar:)
for IACVT vs empty catch-all, which isn't good, and if(foo()? bar : baz() ? qux)
still might be a catch-all with the value baz(): qux
.
The basic takeaway is that you don't want to fight with wide grammars, they're a lot slippier than you'd think. Always just pin them down exactly.
Could the IACVT vs empy be solved via something like valid-empty
keyword, see https://github.com/w3c/csswg-drafts/issues/10441, and treating the actual empty in this case as IACVT? That will make it behave differently from how custom properties currently work, but I find it acceptable. And, as expressed in that other issue, I consider an explicit keyword a better design for this fringe edge case anyway. Especially, given most of the usage for the empty value right now are for conditionals :D
Re: the problem with the <declaration-value>
, I'd +1 to @LeaVerou for disallowing :
from it.
The second, tho, is unresolvable. In
if(foo(): bar, baz(): qux)
,baz(): qux
is a perfectly valid<declaration-value>
! So it's impossible to tell whether it was meant to be "two clauses with no catch-all, evaluating to eitherbar
,qux
, or IACVT" or "one clause with a catch-all, evaluating to eitherfoo
orbaz(): qux
".
My optional-else proposal tries <cond> ':' <declaration-value>
first, and if that matches, treats it as a clause, even if the whole thing also matches <declaration-value>
. So this example would be "two clauses with no catch-all" (given a reasonable behavior for https://github.com/w3c/csswg-drafts/issues/10818). If you want it the other way, you'd prefix with else:
.
Though it might need to be [ <cond> ':' <declaration-value-without-colon> ] | <declaration-value>
. So basically any catch-all value with a :
requires an else:
.
if(foo(): bar, valid-empty)
is longer than if(foo(): bar, else:)
, so if the concern was terseness that's worse. It also requires us to reserve a new global keyword, which isn't needed in any other arbitrary-substitution function that wants to resolve to empty for some case.
It also doesn't solve the bigger problem of "can't tell whether the last clause is a catch-all or not".
I'd +1 to @LeaVerou for disallowing : from it.
Banning :
doesn't fix anything, you need to ban ?
instead. And I'm relatively firmly against imposing arbitrary restrictions on the decl-value, which aren't present in any other arbitrary-substitution function, just to avoid having to type else:
when you have a catch-all (like almost every other conditional in the world does).
My optional-else proposal tries
':' first, and if that matches,
We'd have to put some really wide restrictions on what the un-prefixed decl-value
can contain, to avoid things like authors using a new condition type being treated like a catch-all in older browsers.
So basically any catch-all value with a : requires an else:.
Yeah, I think that's the wideness we would need.
But I'd prefer to push back on these special restrictions just for the purpose of avoiding else
. I think your proposal works if we're forced into it, but I'd like to see if the WG actually feels like this needs addressing first before accepting the compromise.
I spent some time thinking about what I, personally, would want to see as a syntax, without thinking about any potential technical limitations, and so it would fulfill most of the use cases mentioned here.
Here is what I got:
Single branch: bar
when isFoo
is truthful, IACVT otherwise.
if(isFoo: bar)
the same as
if(isFoo: bar;)
empty (\s*
) after the semicolon is not considered as an empty, will be treated as IACVT if isFoo
is falsy.
Two branches: bar
when isFoo
is truthful, baz
otherwise. Only use a ;
here, do not accept a comma.
if(isFoo: bar; baz)
Same, with an explicit else:
branch:
if(isFoo: bar; else: baz)
Same, with an optional semicolon in the end, formatted with newlines:
if(
isFoo: bar;
else: baz;
)
This is the reason I want to have only a semicolon, without a comma as a delimiter here, as well as not treat the emptiness after the semicolon as an empty value — this notation looks very close to what we have in CSS with declarations and descriptors.
The optional else:
allows for a more concise form when needed, but also a more explicit one, which could also be used for the empty fallback case:
if(isFoo: bar; else:)
Same as
if(isFoo: bar; else: ;)
This will also allow, and work very well in how it would look for multiple conditions:
if(
isFoo: bar;
isSomethingElse: baz;
else: boo;
)
Note: I, personally, don't have a strong preference for any of these variants; I equally like the ? :
syntax, and the more expressive else
, but I dislike the comma-semicolon disambiguation, as I think it is very difficult to parse for authors, and an enforced semicolon in case of conditions is very easy to remember, as authors are used to writing the code this way (and an optional omitting of the else:
is just a nice shortcut).
hmmm yeah, I might agree that in this case going straight for a semicolon is the answer, given the rest of the structure so closely resembles the prop: val;
syntax we're used to. The comma-upgrading is great when it's just a list of values, so it can look like a normal CSS function with simple values, but in this case the syntax is already funky. And that avoids the "one clause with a comma-containing value, with IACVT if it doesn't match" problem I'd cited earlier, which is nice and elegant.
I also agree that allowing the final clause to end with the semicolon, and having that not mean anything semantic (just like in rules) is good.
However, the "no else, just implicit catch-all" still fails due to the problems I cited previously - can't distinguish an empty catch-all from no catch-all, and can't distinguish a final test clause from a catch-all. You'd need to make the "bare catch-all" grammar specifically exclude some things and still have else:
to let you write that stuff, like Anders suggested. Having to navigate those restrictions just doesn't feel worthwhile, versus just requiring the else:
on the catch-all clause.
So yeah, I think I support just going straight to using semicolons in this case. That means a grammar of:
if() = if( <<if-clause>> [ ';' <<if-clause>> ]* ';'? )
<if-clause> = <<if-cond>> ':' <<decl-value>>
<if-cond> = <<boolean>> | else
Chair hat off, I like the semicolons and requiring else
.
But wouldn’t the grammar above allow more <<boolean>>
s after the else, or multiple elses?
Not opposed to requiring else:
due to the 2nd reason provided by Tab, but I don't get the problem of the trailing comma/semicolon changing the meaning. It already happens with var()
:
background: green var(--invalid); /* IACVT */
background: green var(--invalid,); /* green */
Edit: Resolved to pursue this! 🎉
There are several issues across this repo proposing some kind of inline conditional function. Here is a sample:
Yet, this is another case where progress has stalled because we’re trying to flesh out a much more general and powerful feature, which involves a significant amount of design & implementation effort. Meanwhile, this major author pain point remains unsolved, and authors have to resort to HTML attributes instead (as explained in #5624).
The current workarounds are:
calc(var(--test) * var(--if-true) + (1 - var(--test)) * var(--if-false))
var(--test, var(--if-false))
However, there is no workaround for transforming arbitrary keywords to arbitrary values, even simple values. E.g. custom properties like these are impossible to implement (examples inspired from the presentational attributes found in Shoelace, one of the most popular web component libraries, but similar use cases can be found in almost any design system and/or WC library):
--variant: auto | primary | success | neutral | warning | danger
--effect: none | pulse
--button-style: fill | outline
--shape: rect | pill
--avatar-shape: square | rounded | circle
--size: small | medium | large
--suffix: none | caret
`--popup-placement: [top | right | bottom | left] [start | end]?
--popup-arrow-placement: center | start | end
What if we could come up with an MVP that could be implemented fast and extended later?
We could scope it down quite a lot and still have something that addresses the most pressing author pain points. Some example restrictions we could start with:
style( <style-query> )
conditionalsEdit (June 10th, 2024):
I had a chat with @andruud a few days ago, and he said that this proposed design would be fairly easy to implement.
Grammar:
<container-query>
<declaration-value>
I did not discuss this but a nice DX improvement could be this grammar instead:
which would allow for multiple conditions more easily. Compare:
Grammar 1:
Grammar 2:
Behavior: Any invalid value would make the property IACVT.
Does it cover enough use cases? I think so. Ideally, you'd want a mechanism to fallback to whatever value the declaration would have normally if nothing matches, instead of IACVT, but that’s much harder to implement.
Implementors, would this make it tractable? If not, what would?