Re: [csswg-drafts] [css-variables?] Higher level custom properties that control multiple declarations (#5624)

Trying to write up an Unofficial Draft on this, I've come across a few issues. @tabatkins and I had a good discussion yesterday about them. I'm going to try and summarize the current status here.

### How to implement `@if`

Pseudo-classes are out of the question, since they match at a completely different point and would cause architectural issues otherwise. 
If we make `@if` cascade, we'd need to carry invisible extra context with each property, which is a significant increase in complexity across every declaration, whether it's inside a conditional or not.

It looks like the easiest way to implement `@if` is my original idea of desugaring it into inline `if()` calls, but also taking into account any properties defined within the same rule as the `@if` block. E.g. this:

```css
.button {
 border-radius: 2px;
 @if (var(--pill) = on) {
  border-radius: 999px;
  padding: 0 1em;
 }
}
```

desugars to:

```css
.button {
 border-radius: if(var(--pill) = on, 999px, 2px);
 padding: if(var(--pill) = on, 0 1em, unset);
}
```

### Avoiding partial application

There are certain values in CSS that evaluate differently depending on which property they are specified on. The obvious one is percentages, but also `em`, `rem`, `lh`, `rlh`, `currentColor`.

Currently, for a generic inline `if()` function, it makes sense to evaluate the condition in the context of the property it's specified on. However, this means that if we desugar `@if` by using `if()` on each value for each declaration it contains, any relative values used may make the condition true for some declarations and false for others. E.g. consider this:

```css
.foo {
 @if (1em > 5%) {
  width: 400px;
  height: 300px;
 }
}
```

which desugars to:

```css
.foo {
 width: if(1em > 5%, 400px);
 height: if(1em > 5%, 300px);
}
```

Now consider that an element that matches `.foo` is inside a `600px` by `400px` container and has a computed font-size of `25px`; This makes `1em > 5%` evaluate to `false` on the `width` property and `true` on the `height` property, which would make the `@if` partially applied. **We most definitely don't want that.**

One solution we came up with was to define *two kinds of inline conditional functions*: One that works as described above, and is **not** used for desugaring `@if`, and one whose main purpose is for desugaring `@if`, let's call it `property-agnostic-if()` (name obviously TBB) for the purposes of this discussion. That function will evaluate each type within its condition against a predefined property, e.g. `color` for `<color>` values, `width` for `<length>` or `<percentage>`, `font-size` for `em` and so on. This means it will evaluate the same on any property, preventing partial application for `@if` blocks that do not contain nested rules.

However, once [CSS Nesting](https://drafts.csswg.org/css-nesting/) comes into play, partial application becomes a problem again. Many use cases require the `@if` to control multiple rules, which would become possible with nesting. However, since condition evaluation depends on the element context, this could mean the conditional matches for the rule it's defined on and doesn't match for nested rules or vice versa! For example:

```css
.tabs {
 display: grid;

 @if (var(--alignment) = top) {
  grid-template-rows: auto 1fr;

  & > .tab-strip {
   display: flex;
  }
 }
}
```

What happens if `.tabs` has `--alignment: top` and `.tab-strip` has `--alignment: left`? So far we have not found a solution to this. Ideally, we'd want matching to happen at the root rule and everything inside the `@if` block either applies or doesn't, but there doesn't seem to be any reasonable way to desugar that into inline functions. Should we just live with this and caution authors against it? Do note that for the Web Components use cases, a lot of this matching will be in Shadow DOM, which is controlled by the component author. So maybe it's ok? However, once the feature is out, authors will use it in a more general way, and nothing can put that genie back in the bottle.

### Cascading

Inline conditionals will have the [IACVT (Invalid At Computed Value Time)](https://www.w3.org/TR/css-variables-1/#invalid-at-computed-value-time) behavior that we have come to know and love (?) from Custom Properties. Since `@if` will desugar to inline conditionals, it will also fall back to that, which may sometimes be surprising. This means that these two snippets are not equivalent:

```css
.notice {
 background: palegoldenrod;
}

.notice {
 /* Desugars to background: if(var(--warning) = on, orange, unset); */
 @if (var(--warning) = on) {
  background: orange;
 }
}
```

```css
.notice {
 /* Desugars to background: if(var(--warning) = on, orange, palegoldenrod); */
 background: palegoldenrod;

 @if (var(--warning) = on) {
  background: orange;
 }
}
```

This also affects how CSS optimizers combine rules, since combining rules with identical selectors can now produce different effects.

There is also the example [in my comment above](https://github.com/w3c/csswg-drafts/issues/5624#issuecomment-744533906), where even though it makes sense if you think about it, for some reason the result *feels* very surprising. 

-- 
GitHub Notification of comment by LeaVerou
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/5624#issuecomment-746339609 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Wednesday, 16 December 2020 14:05:51 UTC