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-4] Allow an inline way to do "first value that's supported" #5055

Closed
tabatkins opened this issue May 8, 2020 · 21 comments
Closed

Comments

@tabatkins
Copy link
Member

tabatkins commented May 8, 2020

When authors use var(), the UA has to assume that the property value is valid at parse time, and only check at computed-value time. If it ends up not being valid, it becomes invalid-at-computed-value-time, reverting to either initial or inherit.

This, unfortunately, loses us CSS's very useful forward-compatibility feature, where we can specify a property two or more times with different features and get the last one the UA supports. Instead we get the last one the UA supports or that has a var() in it, even if the var()-containing one uses features the UA ends up not understanding!

Making this worse, more features are ending up requiring var()-like parsing - attr(), custom functions, etc - so this will only get more common. It would be pretty unfortunate to lose the forward-compatibility feature for a large swathe of CSS usage.

An author can, of course, work around this with @supports. Unfortunately, it has the same separation/repetition/verbosity issues as MQs do, as explained in #5009.

So, related to #5009, perhaps we could have a "validity switch" in the same way? It'd be one of those "must be the whole value of the property" things, and using the same fallback logic as normal CSS, it would resolve to the last item the UA understands. It would just do it at computed-value time, so post-substitution.

So something like:

:root {
  --fg: last-supported(blue; lch(30% 130 300));
}
.foo {
  color: var(--fg);
}
@tabatkins tabatkins added the css-values-4 Current Work label May 8, 2020
@Loirooriol
Copy link
Contributor

In var() the fallback value is the 2nd argument. So for consistency this seems more natural to me:

:root {
  --fg: first-supported(lch(30% 130 300); blue);
}

@Loirooriol
Copy link
Contributor

It could be good to accept a CSS-wide keywords, e.g.

display: first-supported(foo; bar; revert);

if neither foo nor bar are supported, it would behave as revert, preserving UA styles. Without revert I guess it would just become invalid at computed-value time and behave as unset.

@andruud
Copy link
Member

andruud commented May 28, 2020

CSS-wide keywords need to be the cascaded value, which they won't be in this case. So putting CSS-wide keywords in such a function doesn't work with the current model.

Not saying it's impossible, and we're already effectively reverting at computed-value time (#4155), so perhaps we should go all-in and generalize to all CSS-wide keywords and spec it properly. :P

EDIT (one year later): Since my original comment we have allowed CSS-wide keywords in var() fallbacks, so first-supported(foo; bar; revert) is now totally in line with what already exists.

@FremyCompany
Copy link
Contributor

Strong +1

I have proposed first-of(...) back a long time already, and I think this would make many things much better!

@LeaVerou
Copy link
Member

LeaVerou commented Oct 6, 2021

It'd be one of those "must be the whole value of the property" things

Are there others?

@tabatkins
Copy link
Member Author

toggle(), and now mix()

@Loirooriol
Copy link
Contributor

  • What happens if it's mixed with other things?

    :root { --fg: first-supported(grid; flow); }
    .foo { display: var(--fg) list-item; }

    display: grid list-item is currently invalid, and display: flow list-item is valid, so would it pick flow?

    Or the top post says "must be the whole value of the property", does that mean invalid at computed-value time otherwise?

  • When does it resolve?

    If the usecase is using first-supported() in a variable, and then resolving depending on which property the variable is used, then it must stay unresolved in the variable.

    But what if used in a standard property like display: first-supported(foo, grid)? Does it resolve immediately at parse time? At computed-value time? What about registered custom properties?

  • What if all values are invalid?

    :root { --fg: first-supported(foo; bar); }
    .foo { display: var(--fg, grid); }

    Does display become invalid at computed-value time? Does it fallback to grid?

    .foo { display: grid; display: first-supported(foo; bar); }

    If it's resolved at parse time in standard properties, and all specified values are invalid, is the declaration dropped, producing display: grid? Or display becomes invalid at computed-value time?

  • What if some value contains var()?

    .foo { display: first-supported(var(--foo); grid); }

    Does it behave like display: var(--foo) since that's valid syntax (like in @supports), or would it wait to substitute the variable before checking the validity (like usual for var())?

    Top post says both "using the same fallback logic as normal CSS" and "post-substitution", so not clear.

@tabatkins
Copy link
Member Author

What happens if it's mixed with other things?

Parse failure.

(In your example, it's fine in the custom property, because the custom property doesn't see a first-supported() function, it sees a sequence of tokens. Once substituted and given meaning, it's now not the sole value in a property, and thus is a parse failure (thus IACVT).

When does it resolve?

Parse time. This is a pure functionality substitution for just writing the property twice and using the last one that parses.

(As stated above, it's not resolved in a custom property, as only var() functions are. But once substituted, the late parse-time validation that can trigger IACVT takes over.)

What if all values are invalid?

Parse failure.

If it's resolved at parse time in standard properties, and all specified values are invalid, is the declaration dropped

Yes, since it's a parse failure. ^_^

What if some value contains var()?

Then it successfully parses (so long as the var() itself is valid), and is used. Again, pure substitution for "write it twice".

@fantasai fantasai removed the css-values-4 Current Work label Oct 21, 2021
@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-values-4] Allow an inline way to do "first value that's supported", and agreed to the following:

  • RESOLVED: add `first-valid` and please add an issue to bikeshed the name once we better understnad the scope
  • RESOLVED: add `first-valid` and please add an issue to bikeshed the name once we better understand the scope
The full IRC log of that discussion <emeyer> Topic: [css-values-4] Allow an inline way to do "first value that's supported"
<emeyer> github: https://github.com//issues/5055
<emeyer> TabAtkins: This is trying to address an issue that’s become more prevelant as variables have become more common.
<argyle> right at the good part!
<emeyer> …CSS lets you use new features and fall back to old ones by writing something twice.
<emeyer> …Variables break this. We assume things are valid at parse time, and only find out later whether or not they are.
<emeyer> …This same problem is going to come up with more things that do things at parse time.
<lea> q?
<lea> q+
<emeyer> …Proposal is to allow things to sub in the first thing the UA understands at parse time.
<dbaron> ... has to be the full value of the property
<emeyer> …This will need some clarification about how it can or can’t be nested. So we’ll want to define some contextual stuff.
<emeyer> …Overall it’s an attempt to get parse-time fallback behavior.
<fremy> huge +1 of course
<astearns> ack lea
<emeyer> lea: This would be incredibly useful. Would it be availabnle in descriptors as well?
<emeyer> TabAtkins: I don’t see why not.
<emeyer> s/availabnle/available/
<emeyer> florian: So this is no different than writing a thing twice?
<emeyer> TabAtkins: Correct.
<emeyer> emilio: This would go away at parse time?
<florian> s/So this is no different than writing a thing twice?/So this is no different than writing a thing twice if you use it without variables?
<emeyer> TabAtkins: Correct.
<emeyer> emilio: That seems fine.
<emeyer> astearns: Any concerns?
<emeyer> …So the resolution is to add `first-of` to Values. Any objections?
<emeyer> florian: Just wondering about the name of it. If people see this out of context, will they think it’s a list manipulation thing?
<lea> Yeah, as much as I like terse names, first-of() is confusing
<lea> q?
<lea> q+
<emeyer> TabAtkins: It’s possible there will be misinterpretation, but at least they’ll run into confusion quickly.
<lea> q-
<miriam> +1 first-valid
<smfr> +1 on first-valid
<emeyer> florian: How abotu `first-valid`?
<emeyer> s/abotu/about/
<emeyer> TabAtkins: I like it.
<fremy> @emeyer: that was me
<emeyer> fremy: I support that.
<emeyer> astearns: We can bikeshed the name later. Any objections to the idea?
<emeyer> …We are resolved to add `first-valid` and please add an issue to bikeshed the name once we better understnad the scope.
<astearns> RESOLVED: add `first-valid` and please add an issue to bikeshed the name once we better understnad the scope
<emeyer> RESOLVED: add `first-valid` and please add an issue to bikeshed the name once we better understand the scope

@brandonmcconnell
Copy link

I opened a nearly identical issue to this earlier today that thankfully, @Loirooriol caught before I went too far down that rabbit hole.

Originally, I started with the function name, fallback() which admittedly sounds kinda boilerplate-y as far as names are concerned. I chatted with @joshvickerson about this idea on Twitter, and he proposed the function name prefer() which I highly…"prefer" 😅🤦🏻‍♂️… to fallback(), so my vote would be for that name if we're still bikeshedding on it.

Re the functionality, the functionality is nearly identical but likely not 100% the same. Specifically in regarding to var(), my suggestion is generally that var() rather than always assuming a truthy eval would actually use the value of the var() in real-time, so something like this (below) would work as expected and not simply eval to the first var() as would normally happen in the cascade.

element {
  color: prefer(var(--property-1), var(--property-2), var(--property-3), color(display-p3 1 0 0.87), black);
}

This is something I advised against in my original spec proposal, but @Loirooriol very correctly pointed out that simply using identifiers (e.g. --property) without var() could easily be confused with other identifiers, such as counters which can use a similar naming convention. So my new stance would be to enforce using the var() function within prefer() but allow for them to eval to false even without a falsy fallback value.

I think this proposed spec and the one I posted share the same syntax, essentially this:

prefer(value1 [, value2?, value3?, ..., valueN?])

@cdoublev
Copy link
Collaborator

cdoublev commented May 2, 2024

From the comments above, display: first-valid(foo, bar) is invalid at parse time. But the current text says:

If none of the arguments represent a valid value for the property, the property is invalid at computed-value time.


If it is validated at parse time (in standard properties), how should serialize display: first-valid(grid)? Should it serialize naked? Should it produce a pending-subsitution value, in order to serialize shorthands and their longhands?


Is it ok that --custom: first-valid(1) first-valid(1) remains valid?

edit: maybe I misunderstood "the custom property doesn't see a first-supported() function, it sees a sequence of tokens" and the above declaration is invalid, so I filled #10340.


Would it be available in descriptors as well?

I don’t see why not.

CSS Values defines the set of valid values for properties and in many other places but it seems sane to consider something invalid when it is not explicitly valid. Besides, most substitution functions in CSS Values 5 cannot be valid in descriptor values.

For example, first-valid(ns1|type, ns2|type) { ... } should presumably be an invalid style rule. However, it might be useful as a <mf-value>. If you also think so, it would be great to explicitly include it in its alternatives, or allow it in prose.

@LeaVerou
Copy link
Member

LeaVerou commented May 10, 2024

I actually ran into a use case today where this would not just be syntactic sugar, but a substantive improvement. In a web component, I have a user-provided --color-space variable and want to interpolate a gradient in that space IFF that is supported (both gradient interpolation in a color space AND that particular color space) or fallback to oklab if that space is not supported, or fall back to no in <colorspace> token if interpolation in a specific color space is not supported at all.

I can do the latter using @supports but not the former, since I cannot use @supports with CSS variables (can't remember if it reads from root or doesn't support them at all, but neither is helpful here).


I’ve also been thinking, instead of the weird "function that can only be used for a property’s whole value" restriction, perhaps it would make more sense to be able to use CSS's regular fallback mechanism by being able to "mark" certain declarations as "don't drop these, I might need them as fallbacks"? More heavyweight to implement, but potentially better DX. There have also been some thoughts about an @-rule that does this in the @nest (nee @group) discussions.

Or, if we keep it as a function, perhaps we can relax the restriction to "only one of these per declaration"? So that one could do things like:

background: linear-gradient(to right first-supported(in var(--color-space), in oklab, ), var(--color-stops));

Instead of this that the current proposal requires:

background: first-supported(
	linear-gradient(to right in var(--color-space), var(--color-stops)),
	linear-gradient(to right in oklab), var(--color-stops)),
	linear-gradient(to right, var(--color-stops))
);

@ydaniv
Copy link
Contributor

ydaniv commented May 10, 2024

I really needed it many times trying to get something like first-supported(100lvh, 100vh)

@LeaVerou
Copy link
Member

I really needed it many times trying to get something like first-supported(100lvh, 100vh)

This is not as useful for static things like that, as you can just do:

:root {
	--viewport-height: 100vh;

	@supports(height: 100lvh) {
		--viewport-height: 100lvh;
	}
}

Then use var(--viewport-height) throughout your CSS.

@ydaniv
Copy link
Contributor

ydaniv commented May 10, 2024

Right, but it gets messy.

@kizu
Copy link
Member

kizu commented May 28, 2024

Or, if we keep it as a function, perhaps we can relax the restriction to "only one of these per declaration"? So that one could do things like:

We had literally this exact case today, with the gradient interpolation that we wanted to resolve into a “space” when it is not supported. Having an ability to use the first-supported() inside the value would be great.

I guess the main issue with this will be that it should be invalid as a value for non-registered custom properties, so there won't be a way for the authors to specify two instances of it in the runtime? Or we will be ok with making the declaration invalid at computed-value time when we encounter the second instance of the first-supported() in some declaration?

@LeaVerou
Copy link
Member

Or, if we keep it as a function, perhaps we can relax the restriction to "only one of these per declaration"? So that one could do things like:

We had literally this exact case today, with the gradient interpolation that we wanted to resolve into a “space” when it is not supported. Having an ability to use the first-supported() inside the value would be great.

I suspect there are many, many such cases where the potentially unsupported part of a value is a tiny fraction of it.

This reminds me, in that case it should also support empty tokens, so one can do things like linear-gradient(to right first-supported(in oklch, red, lime).

I guess the main issue with this will be that it should be invalid as a value for non-registered custom properties, so there won't be a way for the authors to specify two instances of it in the runtime? Or we will be ok with making the declaration invalid at computed-value time when we encounter the second instance of the first-supported() in some declaration?

I think the latter is far more flexible. Making it invalid in custom properties reduces its utility quite a lot.


Also, any chance we could call it supported()? I think that's equally clear, and much more succinct.

@Loirooriol
Copy link
Contributor

I suspect there are many, many such cases where the potentially unsupported part of a value is a tiny fraction of it

Sure, but it doesn't escale well. If you can put it in the middle of the value, it seems you can use it multiple times for different components, but that may not be well defined in general.

any chance we could call it supported()

I think that would be a very confusing name. A clearer shorter name would be fallback()

@LeaVerou
Copy link
Member

I suspect there are many, many such cases where the potentially unsupported part of a value is a tiny fraction of it

Sure, but it doesn't escale well. If you can put it in the middle of the value, it seems you can use it multiple times for different components, but that may not be well defined in general.

That's why we have IACVT.

any chance we could call it supported()

I think that would be a very confusing name. A clearer shorter name would be fallback()

That seems fine too.

@brandonmcconnell
Copy link

brandonmcconnell commented Jun 2, 2024

I originally proposed fallback() but have grown to appreciate the verbosity of first-supported() as its meaning is unmistakable, though I agree that something like fallback() or prefer() would be a simpler and shorter name if we think this will be used often.

@tabatkins
Copy link
Member Author

Edited in 5fc63f5, present in https://www.w3.org/TR/css-values-5/#first-valid

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

No branches or pull requests