w3c / csswg-drafts

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

[css-values]: Express conditional values in a more terse way #5009

Open jonathantneal opened 4 years ago

jonathantneal commented 4 years ago

Which active (or yet reviewed) proposals might allow CSS authors to express @media conditional values in a more terse way?

The following hypothetical code was widely shared among Twitter users this last week:

A picture shared on Twitter of code, which is written out after this link

@media (max-width: [1200px, 768px, 425px]) {
  .text-box {
    font-size: [24px, 20px, 16px];
    padding: [50px, 30px, 10px];
    margin: [50px 25px, 25px 12.5px, 12.5px]
  }
}

This particular hypothetical solution is described by the author as a “media query [that] is an array of sizes and your property value arrays are linked to those sizes*.

tabatkins commented 4 years ago

If so, 3 just seems like a more general version of 2, rather than "none of them can reasonably be folded into the other". Or am I missing something?

Missing something. ^_^

(2) is a math function - it has access to the values that math functions do, and can compare them like like min/max can. Its values are calculations; they'd live under the same restriction as min() - the "types" of the calculations need to add together without returning failure. So you can tell at parse-time what type the function is, and can grammar-check it, just like the other math functions.

(3) is basically a var() that has access to LayoutWorklet information to figure out which arbitrary value to replace it with; it can do anything, but can't be grammar-checked at parse-time, and it has several properties it can't be used in.

bkardell commented 4 years ago

fwiw, while I realize I have a bias here, @tabatkins offered three names make a lot of sense to me.

javifernandez commented 4 years ago

I did some experiments and I think the approach described by @emilio shouldn't be too complex to implement for Blink and WebKit, so perhaps it'd be a good idea to prioritize this proposal.

We have been discussing at Igalia about this and we think the 3 proposals are useful to web authors for different things. So we were considering integrating these use cases as part of the prototype we are implementing in Chromium for our 'switch' proposal.

tabatkins commented 4 years ago

(The "approach described by @emilio", btw, is the nth-value() proposal I collected in my comment.)

Crissov commented 4 years ago

@tabatkins is correct in that nth-value() (known as index() elsewhere) would be “almost certainly var()-like”. I think that in most cases, authors would keep both, the numeric index (or possibly textual key) and the set of values, in variables. Therefore, it would make sense, instead of introducing semicolons as separators inside property values, which would very likely trip off several linters, syntax highlighters etc., to introduce array-like variables, e. g. like so:

:root 
{
  --foo: 1em;
  /* index-value array, list, enumeration, ordered set or sequence */
  --bar[1]: 0.7em;
  --bar[2]: 1.0em;
  --bar[]: 1.4em; /* uses next available index, 3 */
  --bar[1]: calc(sqrt(1/2) * 1em); /* overwrites earlier declaration */
  /* key-value array, map or unordered set */
  --baz[light-low]: red;
  --baz[dark-low]: blue;
  --baz[light-high]: yellow; 
} 
*
{
  --index: 1;
  --key: light-low;
  font-size: arr(--bar, var(--index));
  color: arr(--baz, var(--key));

  /* optional but logical consequences: */
  line-height: avg(arr(--bar)); /* functions that collapse an arbitrary number of values into a single one, either by selection or by calculation */
  padding: arr(--bar space); /* using a list of values with a predefined separator */
  background: linear-gradient(sort(arr(--baz comma), hue));
} 

Iʼm not sure whether css-syntax would allow the square brackets there without modification.

PS: 🏴‍☠️ PPS: I need to read up on WHATWG data structures to get the terms right.

jonathantneal commented 4 years ago

@Crissov, solid point on the linters; I’ll try to help with that. I can see about ensuring this in the current tmLanguage (which I think is) used by Atom and VSCode. I’m working on updating the PostCSS syntax to fully respect the css-syntax, and that powers stylelint. If they all follow the css-syntax spec, that might help out with a significant chunk of the linter issues. I’ll write some tests to be sure, but I believe all evergreen browsers parse simple blocks to spec, and therefore support nested semicolons in declaration values. I think fixing these user-land build-time parsers will be faster than updating browser engines (to support component values as property names, plus however that would impact the CSSOM).

tabatkins commented 4 years ago

I think that in most cases, authors would keep both, the numeric index (or possibly textual key) and the set of values, in variables.

I don't see why this would be the case.

The point of nth-value() is that it lets authors keep the values right at the point of application, and do a minimal amount of work in MQs to set up the index variable.

If you're going to set up all your values as variables anyway, you might as well go put them in your MQ and skip usage of nth-value entirely, right? No reason to introduce the additional indirection.

(Authors can still reasonably use variables in their values anyway, of course, but I don't think it's reasonable to assume they'll use them for all their values.)

jonathantneal commented 4 years ago

I want to chime in to support the syntactical choice of <any-value>.

Would this mean priorities (i.e. !important) become assignable as a result?

Or would they be ignored?

Or would this feature necessitate a new production (akin to <toggle>) that matches <any-value> with the exception of any <delim-token> with a value of "!"?

Or should I expect something else entirely?

johnadney commented 4 years ago

nth-value is what jvm people would call a tableswitch, and is a common optimization of the c-style switch. the other switch proposal is unusual by c standards, but is however a lot closer to what ecmascript calls a switch, and given the user community overlap, is probably going to win the name if both proposals are going to be seriously considered.

at any rate, nth-value/tableswitch seems both extremely useful and relatively easy to do, so i hope there's implementer interest.

jonathantneal commented 4 years ago

Do I correctly notice two distinct features being proposed within all of these suggestions? One being a preferable syntax, and the other evaluate-able features?

For terseness of code, the nth-value() expression seems like the best fit overall, especially when I compare it against the examples for either switch() or @switch). However, both switch() and @switch happen to bring element queries along for the ride. That’s a pretty big deal. So, for me, this starts to smells of a false choice, where I’m comparing a syntax feature (terseness as readability) with a characteristic feature (element queries).

jonathantneal commented 3 years ago

2. The one mentioned by Lea, which is a math function you provide pairs of calc()-ish comparisons and calc()-ish results, and it resolves to the first value whose comparison succeeds; this can be used anywhere a calc() can. (Just like the other math functions, we can tell what its type will be at parse-time by examining the calculations and grammar-check that accordingly, so it doesn't need to be var-like.)

margin-left: cond((50vw < 400px) 2em, (50vw < 800px) 1em, 0px);

Hey @LeaVerou, in your example, did you intend to reference a quality of the element? I’m unsure at the moment how the above example (50vw < 400px) would do this, but in your example (100% < 50vw), I’d presumed the 100% referred to the element width. However, I was talking with @bkardell earlier today, and we realized we didn’t agree on what we thought you meant. 🤷 I think Brian is up to things related to this, and we’d all want the ideas represented as the authors intended them. 😃

LeaVerou commented 3 years ago

@jonathantneal 100% would refer to whatever percentages resolve to in the property. If the property is margin-left, indeed they resolve relative to element width.

tabatkins commented 3 years ago

Yeah, cond() is basically a "math function", interpreting its values in the same way and with the same timing as any other math function, so 100% is interpreted identically to how it would be in a calc().

mirisuzanne commented 3 years ago

Based on my understanding of cond() and switch(), I'm not yet convinced that they are distinct. But maybe I am also missing something. 😅

In a similar way, it seems to me that the var-like behavior and the property-limitations of the switch proposal are inherent to the value being queried, not the function itself. Based on my conversations with @bkardell, it sounds like the proposed available-inline-size query has certain restrictions, but other future query values might hook into other phases of the rendering lifecycle, with different constraints.

It also seems to me that cond((100% > 500px) …) and switch((available-inline-size > 500px) …) would usually mean the same thing. The only difference I see is that 100% takes different meanings in different properties, where available-inline-size always means the same thing (but only available in certain properties).

Is it possible to separate the proposed functions (eg cond() or switch()) from the element-query values (eg available-inline-size), and think of those as distinct features that can be used together in certain ways? Could we have a joined cond/switch function with math-like behavior, and then also provide var-like lifecycle-values with their own limitations-per-property?

tabatkins commented 3 years ago

There's nothing special about "var() in calc()" - it's identical to var() anywhere else, and makes the property subject to IACVT.

The point is that calc() (and math functions in general) do not do that - they're exactly like any other ordinary value. There's no significant difference, functionality-wise, between width: 10% and width: calc(10% + 2px), or any other math expression. Both of them are validated at parse-time (and so can be thrown out if invalid), and calc() doesn't introduce any dependencies between properties beyond what units themselves do.

It also seems to me that cond((100% > 500px) …) and switch((available-inline-size > 500px) …) would usually mean the same thing.

"Usually the same thing" and "exactly the same thing" are a very wide gulf here. ^_^ The cond() is exactly as troublesome as a calc() would be - width: calc(100% + 50px) and width: cond(100% > 500px; 20px; 30px) both invoke the exact same machinery at the exact same time, and as far as the layout algorithms are concerned, they're identical - they're both functions that (in this case) involve a percentage, so in certain situations they'll behave as auto, in others they'll initially resolve the % to 0 and later resolve it against the true size, etc. No fundamentally new machinery is involved.

Compare with width: switch((available-inline-size > 500px) ...). available-inline-size should always resolve to a correct value; it's specifically meant to work in the kinds of situations that %s don't work in. It resolves "late" and causes its further internal layout to pause for it, until it knows for sure what its available inline size is.

mirisuzanne commented 3 years ago

Right. Those were exactly my points. :)

And I think they back up my argument that we can consider the switch() function & the available-inline-size value as two distinct features that work together – exactly like calc() and var() work today:

  1. A math-like function that is entirely identical to cond() and works on ordinary values without IACVT
  2. New values like available-inline-size which would be useful in a function like that (and maybe also on their own?), but would trigger IACVT

One function can handle both switch() and cond() use-cases, depending what values are used for comparison. The need for IACVT is tied to the values, not to the function itself.

mirisuzanne commented 3 years ago

Notes from an offline conversation with Tab:

Which means that cond() provides a subset of switch() use-cases – and only a subset of those cases would allow parse-time fallbacks (avoiding IACVT). That sub-subset (ordinary number input & output) is the only use-case where the two functions differ.

I'm not convinced that distinction warrants shipping two otherwise-identical-looking functions. We would need to teach authors that yes, switch() can handle all the use-cases, but in very particular circumstances cond() might provide a nicer fallback path, as long as you never put a variable inside it. I don't believe IACVT is worth that much effort to avoid.

Of course @LeaVerou's initial proposal was based on a much simpler ternary if(<condition>, <true>, <false>)… Simplifying the math function that dramatically, it might provide a more distinct/useful shortcut. That proposal makes more sense to me than the middle-ground cond(). Still, it's not much simpler than using switch as a ternary:

.example {
  gap: if(100% > 500px, 2em, 1em);
  gap: switch((100% > 500px) 2em; 1em);
}
LeaVerou commented 3 years ago

Agreed with Miriam. Also, I know these names are pre-bikeshedding, but just saying, I think switch() is a fairly confusing name to non-programmers. And I'd avoid abbreviations like cond().

Of course @LeaVerou's initial proposal was based on a much simpler ternary if(<condition>, <true>, <false>)… Simplifying the math function that dramatically, it might provide a more distinct/useful shortcut. That proposal makes more sense to me than the middle-ground cond(). Still, it's not much simpler than using switch as a ternary:

.example {
  gap: if(100% > 500px, 2em, 1em);
  gap: switch((100% > 500px) 2em; 1em);
}

Just here to mention that I started an unofficial draft about this here: https://drafts.csswg.org/css-conditional-values-1/#if It's very very very rough right now, and I would really welcome feedback for fleshing this out further. Especially from @tabatkins who has specced all the other math functions.

I've just re-read the entire thread, and I can't fully understand the difference between if(), cond() and switch() besides allowing for "else if" values, which is syntax sugar. Is it that switch() accepts MQ-style conditions? If so, we don't need three different functions for expressing conditionals, once we have a <condition> value, we can have different syntaxes/functions that return <condition> to be used in if(). So e.g. we could have a media() function that accepts a media query and returns a <condition> which can be used anywhere a <condition> is accepted. And later a container() functions for returning <condition>s based on container queries. Or even an supports() function for terser @supports. We don't want separate if() functions for every type of condition we may want to specify, do we? Also, just like any other value type, conditions can be assigned to variables, to avoid repeating lengthy conditions in multiple places, which is another problem that was mentioned in the thread.

tabatkins commented 3 years ago

cond() is a math function, so it can be parse-time verified, and because its output is limited to math values, it can have a useful TypedOM representation. switch() is a var-like function, so it must be assumed to be valid at parse time, triggering IACVT if not, and its output is freeform CSS, so all it can offer in TypedOM is a CSSUnparsedValue.

nth-value() is functionally identical to switch(), but is a shortcut for the simple case of just wanting to select from a list of N values. It's intended to let you heavily use MQs without having to shard your styles (duplicating selectors, etc) across multiple MQ blocks; instead, you can use the MQs to set a single custom property, then write your styles once in your normal stylesheet and use nth-value() to select which one gets activated.

faceless2 commented 3 years ago

A definite +1 to a math function, in the sense that it's evaluated at the same time and way as calc() - that way I'd expect it to be very easy to implement.

I'm wondering if it's necessary to limit arguments 2 & 3 to math-type values? Clearly that's how it will be used most of the time - really all the comments but https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-625562169 are considering this general issue for lengths or numbers.

Math functions can already be integers, lengths, angles etc, and if the type is invalid where used, the function itself is invalid. I realise it might mean changes, but I can't see any reason why extending that to strings and colors, for example, would be impossible. I'm not entirely convinced it would be useful, but that's to be determined.

Trying to imagine some cases where wider types would be used, but the only ones I can come up with are pretty contrived:

#phone-number {
    background-color: if(9ch > 200px, #F00, #FFF);
}
.greeked {
    font-family: if(1em < 6px, "greeked", "sans-serif");
}

Although I have a sense of foreboding about that last one as it would change the values for "ex", possibly leading to a loop. Maybe I'm talking myself out of this, but still worth considering before they're ruled out.

(and if this the bit where we bikeshed names then I'd opt for if(5em<200px, 2em, 1em) over cond())

LeaVerou commented 3 years ago

Oh absolutely, <any-value> is a must for anything like this, in fact I have examples with keywords and <image> values in my proposal. In fact, I'm not even sure about the comma-based syntax because that makes it impossible to specify values that include commas, so an alternate syntax I'm considering is keyword-based:

flex-flow: if(100vw > 500px then row else column);

@faceless2 Note that in your second example, these are keywords, not strings, so they wouldn't have quotes.

faceless2 commented 3 years ago

Whoops. They're definitely supposed to be strings, but "sans-serif" was the wrong choice - let's pretend I wrote "Arial" there. But actually I'm not sure about substituting <any-value> myself, because of the difficult typing it - that's why I'd stuck with strings and colors. The same issue applies for attr() and general tokens are excluded there for the same reason.

A nonsensical example to illustrate this is: font: 12px if(100vw > 500px, bold, serif) sans-serif. You can't evaluate any of this at parse time, which is where calc() is evaluated. So while I love the concept and the flexibility if offers, it's a totally different mechanism to the "math-like" version of the solution - the only solution I have an opinion on at this point, because it's so simple to do.

Although I did finally think of a useful example that meets this criteria - layout dependent background images.

background-image: if(width < 100px, url("small.png"), url("large.png"));
tabatkins commented 3 years ago

I'm wondering if it's necessary to limit arguments 2 & 3 to math-type values? Clearly that's how it will be used most of the time - really all the comments but #5009 (comment) are considering this general issue for lengths or numbers.

In theory, no, we could widen the output type somewhat. In practice, that edges closer and closer to having to do combinatorial syntax-checking across all the possible outputs to make sure the property is valid at parse-time. If we want to avoid that, we need to have the possible output types be statically knowable just from the function itself, with no outside context. Colors might be okay, as their syntax doesn't overlap with math expressions at all, but going much wider than (particularly to arbitrary keywords) is probably out of the question. At that point we're dealing with <any-value> and getting var-like IACVT behavior instead parse-time invalidation. And I'm loathe to require authors to do relatively complex type recognition to know which function they're allowed to use; I'd rather have a very simple "is it a number? then you can use cond; otherwise you must use switch" rule.

Alternately, a suite of specialized functions, with the output type baked into the name, would work - if-color(), if-numeric(), if-url(), etc. That still wouldn't give us arbitrary keywords, tho.

LeaVerou commented 3 years ago

@faceless2 Yeah, it would need to have IACVT behavior just like var(). Are there any use cases where that is a problem?

tabatkins commented 3 years ago

Losing the ability to do parse-time fallback, and thus the ability to future-proof your code by providing older, more widely-supported variants, isn't something we should give up lightly. It's been vital to allowing CSS to safely evolve over the years. IACVT behavior was introduced to var() as an unfortunate compromise.

For @faceless2's example, for instance, they couldn't use src() in the function and provide a fallback url() for older browsers.

Also note my comment in https://github.com/w3c/csswg-drafts/issues/5009#issuecomment-785524020; if the output type is var-like than that means Typed OM can't offer any better representation for the output values than CSSUnparsedValue. cond(), on the other hand, can actually provide the output types as a CSSNumericValue, because it knows what type they are.

LeaVerou commented 3 years ago

The ability for parse-time fallback was far more important in the past before @supports. Today, given the increasing popularity of custom properties, it's becoming harder and harder to rely on it anyway. Note that for @faceless2's example cond() still wouldn't work (if I understand it correctly), since src() and url() are not the kinds of simple math values that this would need to be restricted to.

Also note my comment in #5009 (comment); if the output type is var-like than that means Typed OM can't offer any better representation for the output values than CSSUnparsedValue. cond(), on the other hand, can actually provide the output types as a CSSNumericValue, because it knows what type they are.

Why is that? I thought Typed OM returned computed value time representations? If not, there should be something that does.

mirisuzanne commented 3 years ago

I agree with @LeaVerou that var-like behavior is not a problem for authors, even if it was an unfortunate compromise, and that many use-cases (flexbox & grid-templates stand out) rely on any-value output. Parse-time fallback is great, but we also have @supports. I do not believe that authors will spend the time to distinguish between two seemingly-identical functions just to get parse-time fallback in a subset of use-cases. Instead we'd be introducing non-obvious failures, any time an author without full IACVT knowledge (most authors) adds a var() inside an existing cond(), and suddenly breaks the fallback. Using a math function is not a stable promise that you get parse-time fallback. As a teacher, I would always encourage people to use @supports anyway, for either function, just in case.

I like where Lea is going with the if() proposal, designed to handle a wide range of condition-types and outputs.

tabatkins commented 3 years ago

I thought Typed OM returned computed value time representations? If not, there should be something that does.

When you get a computed value, yes. You get also get specified values from the OM! As well, anything with %s probably isn't resolving at computed-value time; those are used-value time.

Using a math function is not a stable promise that you get parse-time fallback.

This argument applies equally well to any syntax - using rgb() is not a stable promise that you get parse-time fallback either, in that case.

As a teacher, I would always encourage people to use @supports anyway, for either function, just in case.

This is a pretty reasonable argument, tho.

joeshub commented 3 years ago

+1 for this array syntax. I have really enjoyed using it already in styled system

mirisuzanne commented 3 years ago

This argument applies equally well to any syntax - using rgb() is not a stable promise that you get parse-time fallback either, in that case.

Exactly. :) My point was that the IACVT ship has sailed at this point, and impacts any CSS that might use variables. No one can rely on any function to consistently have parse-time fallback.

matthew-dean commented 3 years ago

An if()-style function is extremely useful in having a declarative way to choose options. It's how Less libraries embed a lot of conditional logic without more verbose mixin or @if at-rule-like constructs.