Open tabatkins opened 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);
}
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
.
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.
Strong +1
I have proposed first-of(...)
back a long time already, and I think this would make many things much better!
It'd be one of those "must be the whole value of the property" things
Are there others?
toggle(), and now mix()
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.
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".
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
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?])
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.
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))
);
I really needed it many times trying to get something like first-supported(100lvh, 100vh)
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.
Right, but it gets messy.
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?
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.
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()
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.
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.
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 eitherinitial
orinherit
.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 thevar()
-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: