Skip to content

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

Closed

Description

@LeaVerou

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?

Activity

  1. 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?[/+] on Mar 12, 2024
  2. jjenzz commented on Apr 13, 2024

    @jjenzz

    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 {}
    }
  3. moved this from Unsorted to Thursday morning in CSSWG June 2024 meetingon Jun 3, 2024
  4. LeaVerou commented on Jun 10, 2024

    @LeaVerou
    MemberAuthor

    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

  5. andruud commented on Jun 10, 2024

    @andruud
    Member

    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.

  6. Loirooriol commented on Jun 10, 2024

    @Loirooriol
    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.

  7. LeaVerou commented on Jun 10, 2024

    @LeaVerou
    MemberAuthor

    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.

  8. LeaVerou commented on Jun 10, 2024

    @LeaVerou
    MemberAuthor
    • 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.)

  9. andruud commented on Jun 10, 2024

    @andruud
    Member

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

  10. 138 remaining items

  11. benface commented on Feb 24, 2025

    @benface
  12. edbaafi commented on Feb 24, 2025

    @edbaafi

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

  13. benface commented on Feb 24, 2025

    @benface

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

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

  14. edbaafi commented on Feb 24, 2025

    @edbaafi

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

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

  15. edbaafi commented on Feb 24, 2025

    @edbaafi

    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.

  16. tabatkins commented on Feb 24, 2025

    @tabatkins
    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.

  17. edbaafi commented on Feb 24, 2025

    @edbaafi

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

  18. edbaafi commented on Feb 24, 2025

    @edbaafi

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

  19. Loirooriol commented on Feb 24, 2025

    @Loirooriol
    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.

  20. edbaafi commented on Feb 24, 2025

    @edbaafi

    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
  21. tabatkins commented on Feb 24, 2025

    @tabatkins
    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.

  22. edbaafi commented on Feb 24, 2025

    @edbaafi

    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.

  23. edbaafi commented on Feb 25, 2025

    @edbaafi

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

  24. tabatkins commented on Feb 26, 2025

    @tabatkins
    Member

    "Token" shows up many times in the Variables spec, and is referring to these concepts. "Empty token stream" should be pretty predictable from those concepts. Notably, the value of a custom property without a grammar associated with it (from @property or the registerProperty() API) is a sequence (or "stream") of tokens.

  25. edbaafi commented on Feb 27, 2025

    @edbaafi

    I think I can read between the lines or go look at Chromium to get your intent but my expectation (or at least hope) is that the spec would speak for itself and answer this question clearly and definitively and that you’d be more open to feedback if things could be read in a way that is potentially unclear. If you think the spec is clear on this I don’t know what else to say. But here are various things the spec says on this topic in case you are willing to give it a second look (letters a,b,c and emphasis added for illustration):

    a) The if() notation is an arbitrary substitution function

    b) To resolve an if() function, return the <declaration-value> associated with the first <if-condition> that is true; if none are true, return nothing (an empty token stream).

    c) For each arbitrary substitution function func in value:

    1. Resolve func. Let result be the returned result, and fallback be the returned fallback.
      If no result was returned, set result to the guaranteed-invalid value.

    I think what is intended in (b) above is that what is ultimately resolved is a valid (empty) value as would occur if the author wrote a blank space as the value of a property (e.g. —foo: ;) as was helpfully explained (in order to differentiate this from the guaranteed-invalid value) at another place in one of the specs:

    actually writing an empty value into a custom property, like --foo: ; is a valid (empty) value, not the guaranteed-invalid value

    But since the actual term “empty token stream” was never explicitly defined (I didn’t say the single word "token" was never used) and the phrase "return nothing" was used (see (b)) to explain how to resolve an if() function, how can you say this use of "return nothing" couldn’t be confused with being the same as "If no result was returned" (see (c)) leading the reader to believe that this could result in the guaranteed-invalid value as is explained by the actual steps (again see (c)) for resolving arbitrary substitution functions (which the spec previously stated an if() function is - see (a)).

    Or maybe the latter interpretation is in fact the intent and I got the intent wrong, but that's my point. I have read what you pointed me to and get more of the points now (thank you), but I'd appreciate a little clarity in this thread and in the spec. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions