w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.38k stars 642 forks source link

[css-values-5] What is the MVP for inline conditionals on custom properties? #10064

Open LeaVerou opened 4 months ago

LeaVerou commented 4 months ago

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:

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):

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:


Edit (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:

<if()> = if( <container-query>, [<declaration-value>]{1, 2} )

I did not discuss this but a nice DX improvement could be this grammar instead:

<if()> = if( 
    [ <container-query>, [<declaration-value>]{2}  ]#{0, },
    <container-query>, [<declaration-value>]{1, 2} 
)

which would allow for multiple conditions more easily. Compare:

Grammar 1:

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))
        ),
    )
);

Grammar 2:

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)
);

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?

jjenzz commented 3 months ago

Style container queries: these are great, but only work on descendants.

when i first discovered style queries, i admit i had intuitively tried to do something like this:

@container style(--variant: large) {
  & {
    /* style the container itself */
  }
}

it seems a way to select the container that @container style() matches would be a nice stepping stone but i have nowhere near enough context/history to know if that is doable.

or maybe enabling style queries as part of the @scope api?

@scope style(--variant: large) {
  :scope {}
}
LeaVerou commented 1 month ago

I had a chat with @andruud a few days ago, and he said that this proposed design would be fairly easy to implement.

Edit: updated first post

andruud commented 1 month ago

Yeah, at least if we choose the path forwards carefully, it should be easy. That carefully chosen path could be something like:

Of course we'll need to deal with cycles. e.g.:

div {
  --x: if(style(--y: 1), 0, 1);
  --y: var(--x);
}

But we should be well equipped to handle that.

So if the CSSWG determines that this is worthwhile, it should be quite doable overall.


I suppose it's worth thinking about custom functions here as well, since they also kind of provide "inline" conditionals. It might not be ergonomic to use for the use-cases you have in mind, though.

Loirooriol commented 1 month ago

It would seem a bit strange to me if display: if(--x, nonsense, bullshit) is considered valid at parse time, since we know that both possibilities are invalid.

LeaVerou commented 1 month ago

It would seem a bit strange to me if display: if(--x, nonsense, bullshit) is considered valid at parse time, since we know that both possibilities are invalid.

I’m fine to make it invalid at parse time, but it seems like a lot of work for an edge case. Especially given there might be multiple conditionals for a single property value, so it's a combinatorial explosion to tell if any of them would result in a valid value.

LeaVerou commented 1 month ago
  • Substituting an if() then mostly just becomes an advanced var() substitution, with some conditionals built in to it.

    • The if() function can evaluate exactly what style CQs can evaluate, and nothing else (for now).
  • This means only custom properties are supported in practice. (Despite style() currently being specified with support for standard properties, implementations never picked it up.)

Yup. 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.)

andruud commented 1 month ago

I don't see why we couldn't do supports() and media(), 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.)

LeaVerou commented 1 month ago

I don't see why we couldn't do supports() and media(), 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.)

Fair. So we’ll need a distinct token and not <container-query>. @tabatkins is there any easy way to do that without copying all the logic for and/or/not?

kizu commented 1 month ago

A few more workarounds authors can use right now:

  1. Conditions for CSS Variables — mentioned in the issue, but I want to bring attention to the fact that I wrote this post in October 2016 — right after the custom properties appeared in all major browsers (except from Edge, where it appeared a year later). Author interest for conditions like this was present throughout the years ever since, for example, see DRY Switching with CSS Variables by Ana Tudor from 2018
  2. “CSS-Only Type Grinding” by Jane Ori — a very (I repeat, very) convoluted but working (including in Firefox Nightly) way of using registered custom properties to achieve selecting some value based on an ident.
  3. Before I came up with “Cyclic Dependency Space Toggles”, I did a number of experiments (some of which I think I saw before in some ways, so authors were already reaching for these as possible workarounds for the absence of conditions):
  4. Outside of using 1 or 0 values it is possible to work around the absence of conditions for <length> by using rather cumbersome but working calculations as a way to compare lengths: https://codepen.io/kizu/pen/WNBEKvW
  5. Not yet working in Firefox due to a bug, but a possible way to conditionally detect if some custom property is not initial on the element itself: https://codepen.io/kizu/pen/zYQdamG — via an animation that applies space toggles for the default state.

I recommend going to the linked articles and codepens to witness the hacky CSS that is currently required to achieve those conditionals.

Given how often authors reach out to things like space toggles or come up with more and more convoluted ways to achieve conditions for different use cases (Is a custom property defined? Is value A is smaller than B? Are those two conditions true? Etc.), we really need at least some way to have these kinds of conditions.

Ideally, I'd want to have both inline conditions, but also an at-rule-level ones, but even if we will resolve only on inline conditions it will improve the lives of authors tremendously.

tabatkins commented 1 month ago

is there any easy way to do that without copying all the logic for and/or/not?

Unfortunately not, you gotta just write out the full grammar. It's mostly copy-pasting tho.

Loirooriol commented 1 month ago

there might be multiple conditionals for a single property value

Ah, I was thinking of a <whole-value>. If it's not, then it makes more sense to accept everything, yes.

frivoal commented 1 month ago

If we go with grammar 2 (which I think is the better grammar), I'd note that most programming languages don't call that construct "if", but rather "switch", "select", "case", "cond"… I'd suggest going with "switch", since that's what it's called in JS.

css-meeting-bot commented 1 month ago

The CSS Working Group just discussed [css-values?] What is the MVP for inline conditionals on custom properties?.

RESOLVED: Add if() to css-values-5

The full IRC log of that discussion <astearns> zakim, open queue
<Zakim> ok, astearns, the speaker queue is open
<fantasai> leaverou: Motivating use cases
<fantasai> leaverou: right now web components libraries introduce tons of presentational attributes
<fantasai> leaverou: because custom properties include parts of values
<fantasai> leaverou: you can transform numeric values using calc() but keywords not possible
<fantasai> leaverou: Canonical example is changing background-color based on a `--variant` property
<fantasai> leaverou: These are examples for why I started shoelace library now called WebAwesome
<fantasai> leaverou: [lists many examples of utility classes]
<fantasai> leaverou: Style queries get us a lot of the way there, but because they only work on descendants
<fantasai> leaverou: that doesn't let us get all the way there
<fantasai> leaverou: so these remain presentational attributes
<fantasai> leaverou: but needing to branch on a condition is very common in CSS
<fantasai> leaverou: several issues last few years about how can we do this
<fantasai> leaverou: either as a block conditional
<fantasai> leaverou: or inline
<fantasai> leaverou: there are some extremely hacky workarounds that authors use
<fantasai> leaverou: e.g. stting custom property to an empty token string so it gets a value or falls back. Very very hacky things
<fantasai> leaverou: Despite huge demand there's no progress
<fantasai> leaverou: I opened this issue to explore what is still useful, but still easy for implementers
<fantasai> leaverou: Anders said if we re-use the conditional from container queries, that let's you compare values
<fantasai> leaverou: and even media and supports queries
<fantasai> leaverou: re-use that part and then declaration values chosen based on conditional
<fantasai> leaverou: said it was pretty easy
<fantasai> leaverou: For many of these use cases, instead of having one or two values, you could have a series of subsequent conditionals
<fantasai> leaverou: that's nice to hvae
<fantasai> leaverou: but main thing is single test, can always nest
<fantasai> leaverou: ideally we want a block conditional that applies when matches
<fantasai> leaverou: but that seems to be hard, so let's do inline first
<fantasai> leaverou: because we need it anyway
<fantasai> leaverou: if it can be implemented quickly, would be an easy win
<TabAtkins> q+
<kizu> q+
<fantasai> TabAtkins: This sounds good.
<astearns> ack TabAtkins
<TabAtkins> https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-626072319
<fantasai> TabAtkins: going back to previous thread, this comments lists out 3 variants of conditionals
<fantasai> TabAtkins: This is a variant of item 3
<fantasai> TabAtkins: one important part is these shouldn't be a boolean (true/false), it should be multi-valued so you can provide more than one test
<fantasai> TabAtkins: in simplest case that's true or false, just omit last test
<leaverou2> q+
<leaverou2> q++
<fantasai> TabAtkins: but I want to make sure you can sequence tests
<leaverou2> qq+
<leaverou2> q-
<fantasai> leaverou: it's the second grammar
<emilio> q- +
<fantasai> TabAtkins: sgtm
<astearns> ack fantasai
<astearns> ack leaverou
<TabAtkins> fantasai: I think that the IACVT behavior is not amazing
<TabAtkins> fantasai: so i dont' want us to have authors rely on it all the time bc there's no alt
<leaverou2> q+
<TabAtkins> fantasai: to the extent taht people want to switch on a custom prop, can't we do that with some sort of conditional...
<TabAtkins> fantasai: if you just switch basics based on conditionals you can avoid cycles easily that way
<fantasai> leaverou: Would be useful, but this is orthogonal
<TabAtkins> leaverou: a block conditional *would* be useful, but it's orthogonal
<TabAtkins> leaverou2: same as in JS
<TabAtkins> fantasai: not orthogonal from ux perspective. whether someone uses block or inline conditional is somewhat based on availability
<TabAtkins> fantasai: if you have both you'll use both, but if you have only one you'll shoehorn
<TabAtkins> fantasai: i think we should do both if we can
<fantasai> TabAtkins: isnt' the block version just style queries and media queries?
<fantasai> leaverou: Style queries only work on descendants
<TabAtkins> TabAtkins: getting a style query block *on the element* *requires* IACVT
<andruud> q+
<fantasai> fantasai: Shouldn't be a problem if you have standard properties set based on custom properties
<fantasai> TabAtkins: If you restrit it only to standard properties, that's substantially restrictive
<fantasai> TabAtkins: a lot of cases will want to set more variables
<miriam> q+
<fantasai> TabAtkins: Those will then need to use inline version, because that's a requirement of inline
<fantasai> s/because/which uses IACVT because/
<fantasai> TabAtkins: IACVT triggers if you write something wrong
<astearns> ack kizu
<fantasai> kizu: Wanted to show some worse things
<fantasai> kizu: In October 2016, I was using custom properties to switch between values using calculations
<fantasai> kizu: today you can use comparisons with calculations, if length is bigger or smaller use different value -- only for lengths
<fantasai> kizu: ?? made it possible to use values with custom properties
<fantasai> kizu: using type griding
<fantasai> kizu: a very long chain of fallbacks to registered custom property
<fantasai> kizu: and that let's you get any value back
<miriam> s/??/Jane Ori/
<kizu> https://github.com/w3c/csswg-drafts/issues/10064#issuecomment-2161742249
<fantasai> kizu: The code is very complicated
<fantasai> kizu: authors can now do this
<astearns> q?
<fantasai> kizu: recent article where using layers and [missed]
<fantasai> kizu: taking advantage of revert-layer
<fantasai> kizu: The need for this from authors is very high
<leaverou2> fantasai note that all the workarounds involve var(). So all workarounds ALREADY invoke IACVT. This is just about making the API and ergonomics infinitely nicer
<fantasai> kizu: Also wanted to mention, how could we use registered custom properties to avoid some issues with IACVT
<fantasai> kizu: For those we know what types they can accept, what they can return, might make it more simple
<astearns> ack leaverou2
<astearns> ack leaverou
<fantasai> leaverou2, Yeah. My point is I don't want authors to need to use this MORE because it'll be so much easier, simply because don't have a better alternative
<fantasai> fantasai: I don't think we should be trying to encourage authors to use a bad behavior, rather than trying to make something that works better with the cascade
<fantasai> fantasai: Like we should try to make it work the right way rather than settling for IACVT
<astearns> ack andruud
<fantasai> kizu: Using animations to do this is worse
<fantasai> andruud: For block version, if your conditional contains a registered custom property and you try to style the font size with em units, you'll get a cycle
<astearns> ack miriam
<fantasai> andruud: so we'll still run into cycle problems with block version
<fantasai> miriam: Do those cycles make th whole thing impossible, or do they just mean font-size is invalid and we do the condition
<leaverou2> ;?
<fantasai> andruud: for the block conditional?
<leaverou2> q?
<leaverou2> q+
<fantasai> andruud: I guess we could handle those cases 1 by 1
<fantasai> TabAtkins: dependencies will crop up over time, so hard to block ad-hoc
<astearns> ack leaverou
<fantasai> leaverou2: The block version will be inherently limited, because some values will only make sense on a single property
<fantasai> leaverou2: e.g. em/auto/normal
<fantasai> leaverou2: even if we have block version, still need inline version
<fantasai> leaverou2: Also you can assign conditions to a variable and then use that variable in a function, including media test
<fantasai> leaverou2: so handle breakpoints more easily
<fantasai> astearns: Lots of acknowledgement that this is important to work on
<fantasai> astearns: but also some concern about increasing the IACVT behavior on the web
<fantasai> TabAtkins: I don't believe we can solve anything in this space without IACVT
<fantasai> TabAtkins: so unless we can avoid cycles with some new idea, then we need to move forward with IACVT
<TabAtkins> fantasai: i don't object
<TabAtkins> fantasai: lea, can you outline the proposed syntax?
<leaverou2> border-radius: if(style(--button-shape: pill), infinity);
<ydaniv> Is this something that could later be speced to replace IACVT into a proper behavior?
<leaverou2> Longer example: background-color: if(
<leaverou2> style(--variant: success), var(--color-success-60),
<leaverou2> style(--variant: warning), var(--color-warning-60),
<leaverou2> style(--variant: danger), var(--color-danger-60),
<leaverou2> style(--variant: primary), var(--color-primary)
<leaverou2> );
<fantasai> fantasai: I think it would be better if the comma wasn't used both to separate conditional and value, as well as separate sets of conditionals
<TabAtkins> use a colon!
<florian> q+
<fantasai> leaverou2: seems reasonable. Could maybe use a slash
<bkardell_> +1 TabAtkins
<astearns> ack florian
<fantasai> leaverou2: could even use a ? and be more like JS
<lwarlow> q+
<TabAtkins> suggest we take that syntax question to the issue
<fantasai> florian: suggest using switch() for multiple cases, like JS
<fantasai> leaverou2: The conditionals here chain like else-if
<fantasai> leaverou2: but this expands to ranges, etc.
<TabAtkins> yeah, this is an if/elif/elif/else chain
<astearns> ack lwarlow
<fantasai> s/chain like else-if/chain like else-if, you have to provide the conditional each time/
<fantasai> lwarlow: agree with not using comma for both places
<fantasai> lwarlow: slash would read better
<leaverou2> Just noticed all examples above have no else value. This is another example: `border-radius: if(style(--button-shape: pill), infinity, .2em);`
<fantasai> PROPOSED: Add this to css-values-5
<lwarlow> +1
<fantasai> astearns: any objections?
<leaverou2> +1
<bkardell_> https://www.irccloud.com/pastebin/nrStwApc/
<fantasai> RESOVLED: Add if() to css-values-5
tabatkins commented 1 month ago

The syntax as described makes it slightly impossible to say "else be IACVT" in some cases. (For these examples, I'm gonna assume we use : between the condition and value, btw.)

if(style(...): foo, bar) could be a single value foo, 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; the bar, 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.)

So I think we'll need an explicit syntax for saying "no default condition". You don't usually need it, but occasionally you will, and it might make some cases clear even when it's not strictly required. Like, if(style(...): foo, bar; no-value) or something. (Special keyword, allowed only as the whole value of the final argument, always matches but resolves to the guaranteed-invalid value.)

LeaVerou commented 1 month ago

@tabatkins Why not simply make the last argument mandatory and allow empty values (just like var())?

Compare:

Option 1 (mandatory last argument that can be empty):

border-radius: if(style(--button-shape: pill), infinity, );

Option 2 (default: clause):

border-radius: if(style(--button-shape: pill), infinity / default: );

One advantage of having a special value (e.g. revert-declaration?) is that perhaps we can improve on the IACVT behavior by keeping other declarations around when that is encountered (but probably we can't).

If that’s not an option, I'd rather introduce a different separator than a whole keyword. I’m generally all for prioritizing readability, but readability is a balance: too concise and it doesn't make sense, too wordy and you have to wade through the clutter to understand it. With something that I expect will be used all over the place, conciseness matters quite a lot.

tabatkins commented 1 month ago

hy not simply make the last argument mandatory and allow empty values (just like var())?

That's possible, I just find it less clear to read. I don't really like how var() allowed it.

One advantage of having a special value (e.g. revert-declaration?) is that perhaps we can improve on the IACVT behavior by keeping other declarations around when that is encountered (but probably we can't).

I'm not trying to solve any feature lack, just a syntax lack around the existing features.

If that’s not an option, I'd rather introduce a different separator than a whole keyword. I’m generally all for prioritizing readability, but readability is a balance: too concise and it doesn't make sense, too wordy and you have to wade through the clutter to understand it. With something that I expect will be used all over the place, conciseness matters quite a lot.

We can't invent infinite separators. ^_^ But also, this is something that is rarely needed - like I said, the only case that requires it is when you want to supply a single test, with a positive value containing a comma, and no negative value. If you do want to use it for clarity in other cases, I think it's perfectly readable. For example:


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),
    no-value
);
tabatkins commented 1 month ago

If that’s not an option, I'd rather introduce a different separator than a whole keyword.

Actually, let me state this better: this isn't about the separator. The issue is just that the grammar doesn't allow an empty option (and I don't think it should), so there's one particular case you can't express.

LeaVerou commented 1 month ago

@tabatkins Actually, if we don’t use a custom character for separating the condition from the values like the original proposal, this becomes a non-problem. By the time you get to the first value, you know what your separator is, so there’s no ambiguity in if(style(...), foo, bar).

@fantasai you raised the issue of using a distinct character for separating the condition from the value(s), and I agree this would be nice. However, given the ambiguity this introduces, are you ok with going with a comma/semicolon for both to avoid having to introduce weird new keywords like no-value?

fantasai commented 1 month ago

@LeaVerou I don't understand the problem.

LeaVerou commented 1 month ago

@LeaVerou I don't understand the problem.

If we use a different character to separate the condition from the values (say ? or :), then if(style(...) ? foo, bar) is ambiguous: is foo, bar as a whole the if true value, or is the comma a separator? This is because the first time we encounter the separator is when it separates the if true value from the if false value, and we don’t know when to stop parsing these, so we rely on the separator. However, the grammar for conditions is far more restricted, so we know when we’ve stopped parsing the condition, and the character that follows (; or ,) is the function separator, so if(style(...), foo, bar) has no such ambiguity.

The other ways to address it are to make the last argument mandatory and either:

  1. Allow empty values there (if(style(...) ? foo))
  2. Introduce a keyword to represent an empty token stream (if(style(...) ? foo, no-value))

That said, you may want to use an empty value for the if true condition, so these may be orthogonal (though you can always negate the condition, so that’s just syntactic sugar). We could discuss what that value would look like in #10441 .

claudioc commented 1 month ago

I hate to be that guy, but I think the name of the function itself could be improved, instead of just "if"? For example, SQL has the IIF() function which works like this IIF(expression, value-if-true, value-if-false). In general "if" means branching but here we are not branching, we are "selecting" a value depending on an expression, correct?

tabatkins commented 1 month ago

By the time you get to the first value, you know what your separator is, so there’s no ambiguity in if(style(...), foo, bar).

urgh, i really wouldn't want if(style(...); foo, bar; ...). Gives me aesthetic icks.

In general "if" means branching but here we are not branching, we are "selecting" a value depending on an expression, correct?

This is equivalent to an if/elif/elif/else chain. Whether an if is a statement (just executing code) or an expression (evaluating to some value) varies between languages. In the context where this exists in CSS, it's clearly an expression-level thing; the equivalent of an if "statement" would be an @if rule

Loirooriol commented 1 month ago

@LeaVerou I think it isn't more ambiguous than font-family: random-item(serif, sans-serif, monospace), as mentioned https://github.com/w3c/csswg-drafts/issues/9539#issuecomment-1999706240, which needs some spec edits (unless they have already happened), but then it's well-defined?

Loirooriol commented 1 month ago

One advantage of having a special value (e.g. revert-declaration?) is that perhaps we can improve on the IACVT behavior by keeping other declarations around when that is encountered

Such a thing is being discussed in #5319

tabatkins commented 1 month ago

If we use a different character to separate the condition from the values (say ? or :), then if(style(...) ? foo, bar) is ambiguous:

It's not ambiguous - as I said in my earlier comment, it has a defined parse. Absent any semicolons, commas are function argument separators. (#9539 fixes notwithstanding). It's just that you can't indicate you want a comma-containing value, and then nothing for the false case.

brandonmcconnell commented 1 month ago

@tabatkins Per your example, could we not simply omit a final value to default to no-value

Original example (with `no-value`)
```css /* Original example (with `no-value`) */ 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), no-value ); ```
/* Revised example (without `no-value`) */

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)
);

Omitting a final value would be equivalent to using no-value and may help to avoid introducing a new value:

<if()> = if(<condition>: [<declaration-value>]{1, N}; <optional-fallback-value>?)

Would this grammar paired with semicolon delimiters resolve the ambiguity here, so any of the below expressions are valid?

/* no fallback value defined, equivalent of no-value */
if(style(--x: 1): 1, 2; style(--x: 2): 3); /* 2 values: (1, 2) and (3) */

/* fallback value defined as 4 */
if(style(--x: 1): 1, 2; style(--x: 2): 3; 4); /* 3 values: (1, 2), (3), and (4) */

/* ending with a semicolon could optionally be supported, but it would be the same output as the first example, omitting the value altogether */
if(style(--x: 1): 1, 2; style(--x: 2): 3;); /* 2 values: (1, 2) and (3) */

/* not a primary syntax, but I suppose a space could also pass, which may be useful in more complex situations like when used in inside `var()` */
if(style(--x: 1): 1, 2; style(--x: 2): 3; ); /* 3 values: (1, 2), (3), and ( ) */
jimmyfrasche commented 1 month ago

since this is more of an inline switch than an inline if should the default condition have special syntax like if([...] otherwise: x), if([...] default: x), if([...] *: x)? (I'm using [...] metasyntactically to denote the non-default conditions).

It would make it easier to spot the default when reading

LeaVerou commented 1 month ago

After discussing with @fantasai, we think the best way forwards is:

benface commented 1 month ago

Is : as the separator between if-true and if-false values possible?

LeaVerou commented 1 month ago

Is : as the separator between if-true and if-false values possible?

The question is not whether it’s possible, but whether it’s somehow better. Is there any reason other than looking like JS?

benface commented 1 month ago

It avoids the ambiguity around , that you mentioned. And I think ternaries in lots of languages use ? and :, not just JS.

keithamus commented 1 month ago

Given how , is a predominant token across it feels like a less used token like : would be preferable as it does avoid the ambiguity. That it shares similarity with JS seems to be a nice bonus for simplifying the teaching of the syntax.

LeaVerou commented 1 month ago

Ok, here’s a small scorecard:

Reason Argues for… Points
Consistency with other CSS functions A 2
We’re already going with a different separator for the condition, so the ship has sailed wrt consistency with other CSS functions B -1
: was a poor syntactic choice to begin with. The meaning of : in natural language is to explain something (e.g. "if(): A conditional function"), not to provide alternatives A 1
There is no precedent for both if() and a ? b : c, in every other language it’s either a function or a bare a ? b : c A 2
: could throw off existing CSS parsers that assume : separates properties/descriptors from values. A 1
Consistency with other C-like languages, including JS. B -3
: is less likely to conflict with the value B -2
Total Tie! 0

Can anyone think of any other reasons?

Loirooriol commented 1 month ago

I prefer : as a separator between the condition and the if-true value, instead of ?.

if(
  cond1: val1,
  cond2: val2,
  fallback
)
benface commented 1 month ago

Here's an alternative proposal that doesn't require having an if-false value in the function at all:

background: --if-false;
background: if(style(--override-bg: true), --if-true);

The second declaration would be considered invalid if the if() evaluates to false, falling back to the previous declaration. Maybe that feels more CSSy?

brandonmcconnell commented 1 month ago

I prefer B, which appears to remove all ambiguity here with ,/;.

@LeaVerou Could we support some convention for stacking if contexts without nesting if() function usages?

* {
  /* LESS PREFERABLE ❌ */
  --nested-function: if(style(--x: 1) ? 1, 2 : if(style(--x: 2) ? 3));

  /* PREFERABLE âś… */
  --nested-syntax:   if(style(--x: 1) ? 1, 2 : style(--x: 2) ? 3);
}

Note in both of these examples that no else delimiter is needed and no-value is assumed when no final : else/if-false clause is used.


I'm sure someone else in this thread can probably soundboard me on this, but I think this should be doable. As long as an if/else clause returns a boolean followed by a conditional operator ?/:, we can assume we are using a nested conditional syntax rather than the boolean value itself. A boolean value could technically be a returned value if not followed by conditional syntax ?/:.

Some examples of this:

The third of these examples assumes support for boolean values in CSS. I know these have been talked about a lot, and I believe they may have even been accepted for a future version, but I am not entirely sure. If they have not yet been selected, then the third example would be invalid syntax until boolean values are supported.

LeaVerou commented 1 month ago

@benface That syntax doesn’t work with IACVT.

@brandonmcconnell It’s the second grammar we’re going with, so no nesting is needed anyway.

benface commented 1 month ago

Indeed @LeaVerou, thank you for reminding me how this works. Interestingly, you wrote a related proposal that I hadn't seen shortly before I wrote the above: https://github.com/w3c/csswg-drafts/issues/10443. With !revertable, we wouldn't necessarily need to handle if-false / fallbacks in if(), would we?

LeaVerou commented 1 month ago

Agenda+ to resolve on syntax.

LeaVerou commented 1 month ago

Thinking about this some more, I think we should go with if ([cond ? iftrue : iffalse ]+)

Also, given the author need, this should not be held up by syntax. If we don’t have consensus on how to represent an empty value, or whether the last else value should be optional, let’s just spec a version of if() where the last argument is mandatory, and cannot be empty while we figure this out. Worst case, authors can always do this as a workaround:

--empty: ;
background: if(style(--variant: primary), var(--color-primary), var(--empty));
kizu commented 1 month ago

if([cond ? iftrue : iffalse ]+)

I guess, it should have the # with the comma-semicolon thing we decided earlier? (+ will be repeating it with space, # with commas, see https://drafts.csswg.org/css-values-4/#component-multipliers)

Overall, I like the if(a ? b : c) syntax, as the ternaries are common, and we can't use it without the wrapping if() due to the : being significant in CSS syntax.

That said, I wonder if we could do this similarly with calc(), where we could allow using the ternaries in some other contexts without using the wrapping if()? Like we can use calculations without calc() inside things like clamp() and round(), we could allow using ternaries inside calc() and anything similar.


Re: empty value, I think we should allow just omitting the value with optional whitespace to follow how the --foo:; and var(--foo,) work now: so the if(a?b:) and if(a?:b) should work fine.

brandonmcconnell commented 1 month ago

@kizu @LeaVerou What would be the correct grammar for “ternary delimited” conditions, as it appears the syntax would be closer to the below:

* {
  --prop: if(style(--x: 'one') ? 1 : style(--x: 'two') ? 2 : 3);
}

Would that use a # or + if we’re not delimiting expressions by commas or semicolons?


Btw @kizu I really like your idea of supporting ternary expressions in certain other functions without needing to nest if() similar to how we can use calc()-like expressions inside max() and others iirc.

We would not want to support them in all parenthetical contexts, would we, making if() optional altogether? It would need to be in a function, not simply between parentheses?

* {
  --prop: (style(--x: 'one') ? 1 : style(--x: 'two') ? 2 : 3);
}

I think the explicit nature of if() will be useful for readability, but I was curious of the possibility here.

LeaVerou commented 1 month ago

if([cond ? iftrue : iffalse ]+)

I guess, it should have the # with the comma-semicolon thing we decided earlier? (+ will be repeating it with space, # with commas, see drafts.csswg.org/css-values-4/#component-multipliers)

No, because there are no commas. The separators are ? and :.

Overall, I like the if(a ? b : c) syntax, as the ternaries are common, and we can't use it without the wrapping if() due to the : being significant in CSS syntax.

Also because then we wouldn’t know where the condition ends. I suppose both could be addressed with parens, but once you’re adding parens you may as well add two more characters and make this a lot more understandable.

That said, I wonder if we could do this similarly with calc(), where we could allow using the ternaries in some other contexts without using the wrapping if()? Like we can use calculations without calc() inside things like clamp() and round(), we could allow using ternaries inside calc() and anything similar.

I like the idea, but it’s not part of the MVP. Open a new issue, possibly after we've settled the syntax and specced an initial version? But that is another reason to use ? and : so thanks for bringing it up.

Re: empty value, I think we should allow just omitting the value with optional whitespace to follow how the --foo:; and var(--foo,) work now: so the if(a?b:) and if(a?:b) should work fine.

That makes sense iff the default value is not the empty value. If it is, we should just be able to not include a : at all.

LeaVerou commented 1 month ago

@kizu @LeaVerou What would be the correct grammar for “ternary delimited” conditions, as it appears the syntax would be closer to the below:

* {
  --prop: if(style(--x: 'one') ? 1 : style(--x: 'two') ? 2 : 3);
}

Would that use a # or + if we’re not delimiting expressions by commas or semicolons?

Ah good point, my grammar was incorrect. It would be more like if( [ <condition> ? <if-true> : ]+ <default-value>)

brandonmcconnell commented 1 month ago

@LeaVerou Sure thing, that looks correct afaict.

Also, you addressed the other questions from my comment in your response to @kizu’s. Thanks for the clarifying those!

brandonmcconnell commented 1 month ago

@LeaVerou Is there already a CSS function in existence or in the works that could be used to check whether or not the matched element matches a specified selector, essentially selector narrowing?

For example…

BEFORE AFTER
```css /* without using if() */ .item { --symbol: "🩶"; &::before { content: var(--symbol); } &.favorite { --symbol: "🧡"; } } ``` ```css /* using if() */ .item { --symbol: if(matches(.favorite) ? "🧡" : "🩶"); &::before { content: var(--symbol); } } ```
LeaVerou commented 1 month ago

@LeaVerou Is there already a CSS function in existence or in the works that could be used to check whether or not the matched element matches a specified selector, essentially selector narrowing?

For example…

BEFORE AFTER

/* without using if() */
.item {
  --symbol: "🩶";
  &::before {
    content: var(--symbol);
  }
  &.favorite {
    --symbol: "🧡";
  }
}
/* using if() */
.item {
  --symbol: if(matches(.favorite) ? "🧡" : "🩶");
  &::before {
    content: var(--symbol);
  }
}

Possibly, but I think this warrants a new issue, as it’s introducing a new type of condition, and the whole point of the if() MVP is to reuse existing conditionals.

Loirooriol commented 1 month ago

I think we should go with if ([cond ? iftrue : iffalse ]+)

I disagree. iftrue and iffalse should be allowed to be any <declaration-value>, which includes :, so it would be problematic.

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 don't think that consistency with JS justifies diverging from the above for conditionals.

LeaVerou commented 1 month ago

We should definitely allow any <declaration-value>. But while in theory that could include :, does it though? If not, we could simply disallow it.

I think using ? to separate the condition from the first value and then not using : to separate the values from each other is extremely weird. There is no precedent of such a Frankenstein syntax in any language, ever. I expect authors would get this wrong repeatedly if we go that way.

Loirooriol commented 1 month ago

we could simply disallow it

: is valid in custom properties, so it seems unnecessary limiting to disallow it in conditionals that may be used in said custom properties.

I think using ? to separate the condition from the first value

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 */