Re: [csswg-drafts] [mediaqueries-5][css-conditional-5][css-mixins-1] Order dependent rules and adopted stylesheets (#13041)

After discussing with @sesse, I think we have a way to drop order-dependence *without* nullifying layers:

## Introduction

[Earlier](https://github.com/w3c/csswg-drafts/issues/13041#issuecomment-3491642522), I described a way to determine the "winning" rule for a given `@custom-media`, `@custom-supports`, or `@mixin` name: flatten[^1] out a rule-tree into a list of "conditional definitions", then find the bottom-most definition with its condition met:

```css
@mixin --m() { /* A */ }
@media (width > 200px) {
  @mixin --m() { /* B */ }
}
@media (width > 300px) {
  @mixin --m() { /* B */ }
}
```

Would flatten to:

```css
@mixin --m() { /* A */ }
@mixin if(media(width > 200px)) --m() { /* B */ }
@mixin if(media(width > 300px)) --m() { /* C */ }
```

To determine the winning rule for `--m`, we process this list bottom-up, and return the first rule for which the `if()` part evaluates to true. Any dependencies within that `if()` (e.g. `--my-custom-media`) would then need to be resolved *first*, exactly like how custom properties / `var()` works.

Post-parse stylesheet “processing” (in Blink: “building RuleSets”) would then need to happen in two phases:

* Phase I: Determine the winner for each `@custom-media`/`@custom-supports`/`@mixin` name (as described in this post) and produce name-to-rule maps. (`@apply` never happens here.)  
* Phase II: Process stylesheets normally, using the maps produced in Phase I to resolve any usage of custom media, custom supports, or mixins.  (`@apply` happens here.)

## Layers

With layers, figuring out the winner becomes more complicated. We can no longer just return the latest definition with its conditions met, because there may be other (earlier) definitions in *stronger layers*:

```css
@layer one, two;
@layer two {
  @mixin --m() { /* A */ }
}
@layer one {
  @media (width > 200px) {
    @mixin --m() { /* B */ }
  }
  @media (width > 300px) {
    @mixin --m() { /* C */ }
  }
}
```

We would flatten this to:

```css
@layer one;
@layer two;
@layer one;
@mixin if(layer-stronger(two)) --m() { /* A */ }
@mixin if(media(width > 200px) and layer-stronger(one)) --m() { /* B */ }
@mixin if(media(width > 300px) and layer-stronger(one)) --m() { /* C */ }
```

To *now* figure out the winning rule for `--m`, we again process this list bottom-up and evaluate the conditions. We need to maintain a "current winner" as we process this list, to allow `layer-stronger(one)` to check if it's stronger than the layer of that current winner.

In this example, the steps to figure out the winning `--m` would roughly be (say the viewport width is `400px`):

* Consider the bottom-most entry. `media(width > 300px)` is true, and `layer-stronger(one)` is stronger than the layer of the current winner (there is no winner yet). Set `C` to the current winner.  
* Consider the next entry. `media(width > 200px)` is true, but `layer-stronger(one)` is *not* stronger than the layer of the current winner. Do nothing.  
* Consider the next entry. `layer-stronger(two)` is stronger than the layer of the current winner. Set `A` to the current winner.  
* \=\> `A` is the winner.

To evaluate e.g. `layer-stronger(two)`, we need to figure out the strength of both `two` and the layer it's comparing against (`one`). We do this by searching for the *earliest point of existence* (EPOE) for some layer `X`:

* Go through the list *top-down*, and return the position of the first `@layer X` definition with its condition met.

Doing this for both layer `one` and `two`, we find the EPOE values `0` and `1`, respectively. This means `layer-stronger(two)` returns true, since the EPOE of layer `two` is stronger (i.e. greater) than the EPOE of layer `one`.

This particular example has only *unconditional* layers, but e.g. `@media (--mq) { @layer foo; }` would yield a conditional layer definition like `@layer if(media(--mq)) foo`, and would only count as existing at a certain point with that condition met.

## Cycles

Going back the problematic example from the earlier [comment](https://github.com/w3c/csswg-drafts/issues/13041#issuecomment-3491642522), where the layer order depends on a custom media query, and the truth value of the custom media query depends on the layer order:

```css
@media (--b) {
  @layer y, x;
}
@layer x {
  @custom-media --b false;
}
@layer y {
  @custom-media --b true;
}
```

This now flattens into:

```css
@layer if(media(--b)) y;
@layer if(media(--b)) x;
@layer x;
@custom-media if(layer-stronger(x)) --b false;
@layer y;
@custom-media if(layer-stronger(y)) --b true;
```

When determining the winner of `--b`, we now have an obvious opportunity to detect cycles: when evaluating `if(layer-stronger(y))`, we eventually need to evaluate `if(media(--b))`, which again requires determining the winner of `--b`. This causes `--b` to not be defined here, and we proceed to the next attempt at defining `--b`, under the condition `if(layer-stronger(x))`. This also creates a cycle, and absent any other `@custom-media` rules, `--b` ends up not being defined at all, and the final layer order becomes `[x, y]` via the `@layer` block rules.

If we modify the example a bit, adding a third layer:

```css
@media (--b) {
  @layer y, x;
}
@layer z {
  @custom-media --b true; /* new */
}
@layer x {
  @custom-media --b false;
}
@layer y {
  @custom-media --b true;
}
```

If we repeat the same process, we still find the cycle for the `--b` definition in layer `y`, and we still find a cycle for the `--b` definition in layer `x`. But after that, we try `--b` in layer `z`, which is not in any cycle. This means `--b` is defined as `true` here, and the layer order ultimately becomes `[y, x, z]`.

## Performance

There is no experimental implementation of this yet, so there is also no real data. However, each winning custom media rule, custom supports rule, mixins rule, and layer EPOE has a definite and stable answer throughout Phase I, and can therefore be cached.

## Conclusion

With this approach, authors would not have to deal with novel order-dependent behavior (cc @sorvell), concerns about `adoptedStyleSheets` would vanish, and layering these rules would still work (cc @mirisuzanne, who said this might become common with mixins). However, we would need to unconditionally load `@import supports(--foo)`(just custom `supports()`; non-custom can still block loading),  make `@custom-media/supports` invalid in `@mixin`s (similar to how `@mixin` itself is invalid in mixins), and it does require an extra pass over the rules (cc @emilio).

On balance, the long-term authoring experience seems to benefit if we go this direction instead of order-dependence.  

[^1]: Flattening helped me think about this problem. I don't expect implementations to literally do this.

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


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

Received on Friday, 5 December 2025 09:53:51 UTC