w3c / csswg-drafts

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

[css-values-4] Allow an inline way to do "first value that's supported" #5055

Open tabatkins opened 4 years ago

tabatkins commented 4 years ago

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);
}
Loirooriol commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

Strong +1

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

LeaVerou commented 2 years ago

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

Are there others?

tabatkins commented 2 years ago

toggle(), and now mix()

Loirooriol commented 2 years ago
tabatkins commented 2 years ago

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

css-meeting-bot commented 2 years ago

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:

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/w3c/csswg-drafts/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 commented 1 year ago

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 commented 4 months ago

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 commented 3 months ago

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 commented 3 months ago

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

LeaVerou commented 3 months ago

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 commented 3 months ago

Right, but it gets messy.

kizu commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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.