Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
LeaVerou opened this issue Mar 12, 2024 · 139 comments
Closed

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Mar 12, 2024

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:

  • Using a 0/1 switch and linear interpolation: calc(var(--test) * var(--if-true) + (1 - var(--test)) * var(--if-false))
  • "Space toggle": Using presence/absence and var(--test, var(--if-false))
  • Style container queries: these are great, but only work on descendants.
  • Cyclic Dependency Space Toggles

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:

  • Only style( <style-query> ) conditionals
  • Only custom properties
  • If it helps, the value space could be restricted too, even as much as single tokens
  • If it helps, it could even be something that’s only allowed in custom property values

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} )
  • Links:
  • Values can be nested to produce multiple branches
  • If a third argument is not provided, it becomes equivalent to an empty token stream

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?

@LeaVerou LeaVerou changed the title What is the MVP for inline conditionals on custom properties? [css-values?] What is the MVP for inline conditionals on custom properties? Mar 12, 2024
@jjenzz
Copy link

jjenzz commented Apr 13, 2024

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 {}
}

@astearns astearns moved this to Unsorted in CSSWG June 2024 meeting Jun 3, 2024
@astearns astearns moved this from Unsorted to Thursday morning in CSSWG June 2024 meeting Jun 3, 2024
@LeaVerou
Copy link
Member Author

LeaVerou commented Jun 10, 2024

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
Copy link
Member

andruud commented Jun 10, 2024

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

  • When you see an if() in a declaration's value, you get the "assumed valid at parse time" behavior we currently get when there's a var() or env() in the value.
    • 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.)

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
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Member Author

  • 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
Copy link
Member

andruud commented Jun 10, 2024

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
Copy link
Member Author

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
Copy link
Member

kizu commented Jun 11, 2024

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
Copy link
Member

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
Copy link
Contributor

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

This comment was marked as resolved.

@css-meeting-bot
Copy link
Member

css-meeting-bot commented Jun 13, 2024

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//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//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
Copy link
Member

tabatkins commented Jun 13, 2024

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
Copy link
Member Author

LeaVerou commented Jun 13, 2024

@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
Copy link
Member

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
Copy link
Member

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
Copy link
Member Author

LeaVerou commented Jun 13, 2024

@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?

@LeaVerou
Copy link
Member Author

One thing that was not resolved, but should be non-contentious — that the trailing ; can be optional, and can be present without anything else (this way it will be following the way CSS declarations work already).

That doesn’t make sense, since we are now not using semicolons in there at all.

@tabatkins
Copy link
Member

??? The resolution was indeed to go with colon-and-semicolon.

@LeaVerou
Copy link
Member Author

@tabatkins Oh right, my bad! I thought we had resolved to go with regular argument separators. Hmm. So we’ll have some functions where arguments are separated by commas, some where they are separated by semicolons, and some where they use the pattern we resolved to in #9539 ? That’s …pretty weird 😕

@tabatkins
Copy link
Member

No, we have if(), specifically, that uses semicolons for Reasons; all other functions use commas. Some (very limited list) of functions are capable of containing values that themselves have commas; to write those values you use the {} wrapper if necessary. That's a property of the argument, tho - the functions are just comma-separated like normal.

@fantasai
Copy link
Collaborator

Edits are in, please open follow-up issues for anything that still needs adjustment! Thanks!

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

@LeaVerou @fantasai et al. this is amazing! I'm a bit late to the party but I have two questions:

  1. Can we change the keywords/names from if/else? This feature is amazing but as a programmer those keywords just feel wrong in this context. As we know, in most programming languages if is a keyword associated with a statement (or statement block) not an expression (value). I think the if keyword would make sense if it had an associated declaration block (if(){}) like @media(){} or @container(){} but that's not what's going on here.

So what could we call it? I had a preprocessor based implementation called ternary() that was based on Ana Tudor's space toggles (and the ubiquitous programming language ternary expression i.e. condition ? valueIfTrue : valueIfFalse) that seems to perfectly match what's actually going on here (evaluating a conditional expression to a value):

--color: ternary( style(--type: success), var(--green-60), var(--red-60) );

But this has the issue you seemed to want to avoid where you need to nest multiple ternaries to evaluate multiple conditions (I didn't mind this because as a programmer I'm used to it and the use case was hidden in a library that rarely changed but I understand the motivation to avoid this):

--color: ternary(
  style(--type: success), var(--green-60), 
  ternary(
    style(--type: warning), var(--orange-60),
     var(--red-60)
  )
);

So what could the keywords be?

In a lot of ways your proposed syntax makes me think of switch (e.g. the colons and semicolons). I think using default instead of else makes sense but using switch would have the same issue that it's associated with statements not expressions and then the additional issue that the programming language switch does not allow for multiple conditions, just multiple cases (values). My below suggestion probably doesn't work because of the loaded meaning of selection in CSS but throwing this out as it might lead to something better:

--color: select(
  style(--type: success): var(--green-60); 
  style(--type: warning): var(--orange-60),
  default:  var(--red-60)
);

Maybe choose?

--color: choose(
  style(--type: success): var(--green-60); 
  style(--type: warning): var(--orange-60),
  default:  var(--red-60)
);

That brings me to my second question which might illuminate a better keyword...

  1. What exactly is meant by "empty token stream"? There doesn't seem to be a definition in the draft and a google search for site:https://csswg.org "empty token stream" only brings up the draft itself so where is this term defined and what does it mean exactly?

Thanks for this amazing feature and for your consideration regarding not using a ubiquitous programming keyword with such a well established meaning.

@mindplay-dk
Copy link

@edbaafi for simple conditionals, maybe when could work? seems to fit naturally in this context - "when this condition is true, use this value, otherwise use that value."

for pattern matching, which I think is what you're highlighting as "something better" here, select would clearly be problematic, both in an HTML and CSS context - words like match, switch or case might work for that.

but I don't think pattern matching can or should take the place of simple conditionals - they're for a different use case. (you probably don't want your code littered with "pattern matching" expressions with just a single condition and a default.)

(it would be great to have both, but I suspect pattern matching is even more difficult to fit into existing syntax...)

@Loirooriol
Copy link
Contributor

I don't think the the distinction of expressions vs statement matters much.

In Rust you can use if for both. And ECMAScript has a stage 2 proposal to allow throw (currently just for statements) in expressions, the proposal didn't choose another keyword just because of the context difference.

@mindplay-dk
Copy link

@Loirooriol I guess, yeah, Rust, Kotlin, Lua, Haskell all have if, then, else literals as expressions.

in a web context though? I can't think of anything in a web context that has the if keyword as an expression.

SCSS for example has if blocks:

@if $light-theme {
  background-color: $light-background;
  color: $light-text;
} @else {
  background-color: $dark-background;
  color: $dark-text;
}

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

@mindplay-dk Sorry the "something better" was just referring to a different set of keywords (while names are mostly subjective - "better" here refers to terms that lack the baggage of well known definitions for the target audience such as if or select). I was highlighting that ternary made sense (although also not as accessible of a name for non-programmers) before the choice was made to allow for easy switching (what you're calling "pattern matching"). Scroll up to the OP and you'll see the Grammar 1 vs. Grammar 2 question. What I'm saying is ternary() matches 100% with Grammar 1 (having to nest the if()), but not Grammar 2 (allowing for multiple conditions within one if()) that was what was decided on.

@Loirooriol thanks for pointing that out about rust! It's an expression driven language so that makes a lot of sense in that context. I still think we could pick a name that doesn't mean a specific, conflicting thing to so many developers (especially wrt the only language shipped with the browser). I was only pointing out that it felt wrong to me and trying to make sense of why. If we don't care about the expression vs statement question there is still the fact that we are adding multiple conditions to a single if expression. Does rust or any other modern language do that with if? And then does it matter when JavaScript doesn't and that is the most relevant language for this set of developers?

It seems this feature went through a lot of discussion! And digging through the multiple threads, it seems there was a point that if was being considered for working on declaration blocks. I'm just hoping the keywords can be reconsidered now that it's gone through so many changes including 1) working on only values rather than whole declarations, and 2) grouping multiple conditions vs. requiring nesting.

Also still hoping for an answer to my second question which could help illuminate what exactly the proposed feature is doing which is important when discussing the naming/keywords. If evaluating to this "empty token stream" has no equivalent in any programming language conditional expression/statements, it makes the case even stronger to choose a new, "untainted" name to describe this new functionality:

What exactly is meant by "empty token stream"?

@benface
Copy link

benface commented Feb 24, 2025

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

@benface amazing! Thanks for the info. Hope it's not too late for a keyword change. To be clear I'm not suggesting anything other than swapping the if/else keywords for something else.

Also, will look to canary to see if I can understand the meaning of "empty token stream" in this context.

@benface
Copy link

benface commented Feb 24, 2025

Hope it's not too late for a keyword change.

I hope it is! I really like if / else. 😂

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

I hope it is! I really like if / else. 😂

haha.. I love if / else too just not for where this feature ended up going

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

To be clear I'm not suggesting anything other than swapping the if/else keywords for something else.

Since I've been focused on if let's take a look at how the current syntax changes the typical use of else:

Re-reading the spec makes me see how weird this is:

if( condition1: value1;  condition2: value2; else: value3 )

What other language's use of else puts it within the if's () and has to describe it this way:

The else keyword represents a condition that is always true.

This is why default seems very appropriate for where this feature ended up (all of the reasons it's structured this way makes sense but now we're left with names that don't match the final structure). I'm going to add a third and fourth point to what we're changing from the typical (especially JS) use of if/else.

  1. working on only values (expressions) rather than whole declarations (statements) - rust aside ;)
  2. grouping multiple conditions vs. a single condition and requiring nesting if you want more (or for languages where nesting was undesirable (e.g. python), an additional keyword like elif).
  3. putting the conditions and resulting values within the () e.g. if(condition: value) vs if(condition1) value1
  4. making the else a "special" condition of the if (within the if's ()s e.g. if(condition: value1; else: value2) rather than a statement/expression at the same structural level as the if (e.g. if(condition: value1) else value2)

All this can be avoided by picking a different name for if. I think default as a replacement for else here makes total sense. That way we can say: "The default keyword represents a condition that is always true" in the spec and something like: "We can use default: <value> to provide a value to use when any of the previous conditions were not met" in the docs, instead of: "The else keyword represents a condition that is always true" in the spec and who knows what in the docs.

@tabatkins
Copy link
Member

Can we change the keywords/names from if/else? This feature is amazing but as a programmer those keywords just feel wrong in this context. As we know, in most programming languages if is a keyword associated with a statement (or statement block) not an expression (value).

Yeah, as Oriol said, that's a consequence of those languages being block-oriented, not something super attached to the keywords themselves. More functional languages that feature control-flow expressions usually name their conditional if too. It's perfectly fine for CSS to do so, and I don't think there's a good reason to avoid using if() as the most natural name here.

The "web languages" (aka JS and its compilation variants) all descend from a C-like syntax ancestry, so of course they don't use if in the way we're using it here. CSS is completely divorced from that ancestry and differs in many, many ways from the syntax assumptions of those languages, and will continue to diverge as we add more features that are programming-ish (like, for example, custom functions, which use nested style rules and style application rather than imperative statement flow).

As someone who's learned a little bit of a lot of languages, I assure you that you'll get used to this sort of name reuse with diverging syntax.

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

@tabatkins Thanks for your response, and sure, I think that addresses the first point, so we can disregard that. Again, I think this is a big win in terms of functionality. I think the fact that CSS will get many more "features that are programming-ish" is exactly why this needs a bit more thought.

I think the rust example is a good one because that's a language that has both statements and expressions where a fully functional language like Elm does not. For languages with both (I've been using the analogy that declarations are like statements and everything after the : in a declaration is like an expression), it is important that if can be used for both statements and expressions.

I know this is the MVP for inline conditionals as the post title says, and that so far it's been decided not to provide conditionals for declaration blocks, but if this position changes in the future would you be able to make that use of if make sense or would you be forced to pick a different keyword?

How would the syntax with the current issues I addressed as points 2-4 look with declaration blocks not just values? (I'm not asking us to focus on if this feature is likely but just as a thought experiment to understand the syntax that has been decided on).

<selector> {
    if( style(--type: success) : {
            color: var(--green-60);   
            border-color: var(--green-90);   
        };
        style(--type: warning) : {
            color: var(--yellow-60);   
            border-color: var(--yellow-90);   
        };
        else : {
            color: var(--red-60);   
            border-color: var(--red-90);   
        }
}

So yes, functional languages without statements (and those like rust that seem to try to split the difference) have named their conditional expressions if and sure they work on expressions, but which ones do the things I addressed as points 2-4 (single if keyword for multiple conditions, putting the condition and result in parens (), and nesting the if within the () at a different hierarchical level than the if)?

I think it's great that CSS will get many more "features that are programming-ish" and just think that we should nod to the norms there wherever we can. The point that CSS is different is also well taken, so if a given keyword evokes certain expectations across many languages (again just focusing on points 2-4), shouldn't we find a different keyword?

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

Also, @tabatkins is the meaning of "empty token stream" defined anywhere in a spec?

@Loirooriol
Copy link
Contributor

so far it's been decided not to provide conditionals for declaration blocks

What? Plenty of conditional blocks in these specs:
https://drafts.csswg.org/css-conditional-3/
https://drafts.csswg.org/css-conditional-4/
https://drafts.csswg.org/css-conditional-5/

would you be able to make that use of if make sense or would you be forced to pick a different keyword?

There is no problem if a functional value and an at rule share the same name.
https://drafts.csswg.org/css-conditional-5/#when-rule uses @when because of a clash with SASS, not because of a conflict with inline if(). Also see #6684, @when might still be renamed to @if.

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

As someone who's learned a little bit of a lot of languages, I assure you that you'll get used to this sort of name reuse with diverging syntax.

Sorry, but I'm not writing on my own behalf, this is about meeting users where they are coming from and making the web platform as accessible as possible for the most people. How far do you have to go down your preferred list of languages by popularity (excluding non-programming languages) before you get to one with conditional expressions? But we've excluded that point, so what language anywhere on your favorite list does the other things I've highlighted as points 2-4 above?

The point is I'm not living under a rock because my initial reaction was this felt wrong because of expressions vs. statements (I've used most of the top 10 languages on the stack overflow list below). But I've accepted that point, and I'm hoping my others are not overlooked.

Image

@tabatkins
Copy link
Member

if this position changes in the future would you be able to make that use of if make sense or would you be forced to pick a different keyword?

As @benface said, nothing prevents us from adding @if in the future with whatever syntax we want. At-rules and functions are completely distinct.

(Well, it turns out there is a reason to avoid @if, but that's unrelated to this topic.)

The syntax would definitely be an at-rule, because that's what's allowed at the rule level in CSS syntax. No chance of confusion with value-level functions.

The point that CSS is different is also well taken, so if a given keyword evokes certain expectations across many languages (again just focusing on points 2-4), shouldn't we find a different keyword?

Not necessarily. Different languages express themselves differently. That's fine. "If" is by far the most common word for the basic conditional syntax across languages. This is the basic conditional syntax for CSS. It also doesn't look like any of the less-common conditional syntaxes in nearby languages, like "switch" or "cond" or "match", because those languages are also designing under their own assumptions and syntax precedents.

is the meaning of "empty token stream" defined anywhere in a spec?

Tokens are discussed in CSS Syntax and, more relevantly for this topic, in CSS Variables.

@edbaafi
Copy link

edbaafi commented Feb 24, 2025

Loirooriol thanks for the references, I will go through them. I was trying to follow this discussion which is in so many threads and it was mentioned somewhere (including alluded to in this post's title) that the MVP would be inline only (value vs block). That's all I meant by "so far". Will look at the other proposals and see how far they are.

Tokens are discussed in CSS Syntax and, more relevantly for this topic, in CSS Variables.
Ok not sure why my google search for site:https://csswg.org "empty token stream" didn't find a reference to that but will take a look.

Thanks, I think I get the point about the block version being an @ rule e.g. @ifand being outside of the ruleset. Somewhere in this thread someone showed an @if that was inside of the ruleset's declaration block, and it created its own declaration block. I guess that stuck with me. With CSS nesting creating declaration blocks within declaration blocks, I guess it didn't read it as so off.

nothing prevents us from adding @if in the future with whatever syntax we want. At-rules and functions are completely distinct.

Sure, I would argue for avoiding using the same name for a value function as an @ rule for all the reasons I'm arguing against using if in a way that diverges from its use in many programming languages. Just because the language separates the @ and function() namespaces doesn't mean it's a good idea to use the same name in both.

But if this direction is set, than it's set. Again thanks all for pushing this regardless of my issues with the naming.

@edbaafi
Copy link

edbaafi commented Feb 25, 2025

@tabatkins re: "empty token stream", I think that might be a typo. I read CSS Syntax and as one would expect, tokens are described with respect to parsing:

  1. Tokenizing and Parsing CSS
    User agents must use the parsing rules described in this specification to generate the [CSSOM] trees from text/css resources. Together, these rules define what is referred to as the CSS parser.

Once the CSS is parsed and the CSSOM generated where does this concept of tokens come up again? What do tokens have to do with compute time values?

I also read CSS Variables as you indicated this was the relevent spec. There is no reference to "empty token stream" in either of these documents but CSS Variables does talk about "empty strings" and "empty values" (the latter is what I suspect is meant in the new if() spec so as not to confuse with IACVT which serializes to the empty string):

2.2. Guaranteed-Invalid Values
The initial value of a custom property is a guaranteed-invalid value. As defined in § 3 Using Cascading Variables: the var() notation, using var() to substitute a custom property with this as its value makes the property referencing it invalid at computed-value time.

This value serializes as the empty string, but actually writing an empty value into a custom property, like --foo: ;, is a valid (empty) value, not the guaranteed-invalid value.

Again, the new CSS Values section references "empty token stream" which AFAICT is not defined anywhere else in the spec and doesn't make sense if tokens are only a concern during the parsing step and not at custom property compute time. As you can see the question here is what happens when no condition in if()'s argument list is true, and this cannot be known at parse time, only compute time:

7.3. Conditional Value Selection: the if() notation
The if() notation is an arbitrary substitution function that represents conditional values. Its argument consists of an ordered semi-colon–separated list of statements, each consisting of a condition followed by a colon followed by a value. An if() notation represents the value corresponding to the first condition in its argument list to be true; if no condition matches, then the if() notation represents an empty token stream.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Thursday morning
Status: Thursday morning
Development

No branches or pull requests