[csswg-drafts] [selectors][css-conditional] Pseudo-class that is syntactic sugar for wrapping in an `@rule` — any at-rule (#11969)

LeaVerou has just created a new issue for https://github.com/w3c/csswg-drafts:

== [selectors][css-conditional] Pseudo-class that is syntactic sugar for wrapping in an `@rule` — any at-rule ==
I was reminded of this by [today’s resolution to remove `:host-context()`](https://github.com/w3c/csswg-drafts/issues/1914#issuecomment-2737310093). While I think it was a good call, one issue is that style queries involve an `@rule`, so there is no easy way to combine them with selectors without duplicating code.

My [older proposal for a `:media()` pseudo-class](https://github.com/w3c/csswg-drafts/issues/6247) was fairly well received. However, since then, I have been stumbling on many more use cases that require combining selector logic with @-rule logic, beyond just `@media`. Many examples were mentioned in that thread.

I think all conditional @-rules would benefit from something like this, e.g.:
- `@media`
- `@supports`
- `@container`

But also several grouping rules:
- `@starting-style` was mentioned in the thread above
- `@layer` e.g. at work we have styles (e.g. themes) that are applied both on certain native selectors (e.g. `:root`) as well as via certain classes (e.g. `.palette-foo`). There is no way to assign the rules a layer when used on `:root` but have regular priority when applied via the CSS class, without duplicating all CSS.
- `@scope`, e.g. to combine donut scope (not possible via a selector) with regular selectors.
- Even `@page`, to share styles between a printed page, and the rendering of the printed page on screen

## Proposal

At this point I'm convinced these use cases will keep creeping up, and rather than trying to tailor something to specific use cases, we should design a generic solution that will not need to be updated for every new grouping rule we add.

Possibly a generic pseudo-class (let's call it `:at()` for now, TBB) that operates merely as syntactic sugar and is completely agnostic to the rules it is specifying. Something like:

```
:at(<ident> [ <boolean-condition> || <decl-value> ]? )
```

It does not affect specificity. While it would make sense to make it increase specificity, this ensures that the result is equivalent to doing the transformation manually, so it can be polyfilled via preprocessors.

### Why not do this with a preprocessor? 

Doing this with a preprocessor involves duplicating **a lot** of CSS, which will need to be sent down the wire, while it's a relatively simple transformation for UAs to apply.

### What about doing the opposite: introducing a `@selector()` rule?

Selectors are generally more flexible, more composable, and more well understood. 

### Alternate names

The advantage of `:at()` is that it connects it directly to the `@` syntax. However, it is also misleading, as it looks like a preposition.

Other naming ideas:
- `:if()`: More clear, but could also restrict us down the line if we introduce a different type of conditional
- `:atrule()`: More clear, but also verbose and awkward

## Examples


```css
.foo,
:root:at(layer design-system) {
  /* declarations */
}
```

becomes:

```css
.foo {
  /* declarations */
}

@layer design-system {
  :root {
    /* declarations */
  }
}
```

----

The position of the pseudo-class in the complex selector affects how the rewriting happens, essentially as an extension of nesting:

```css
.fade-in,
.fade-out &:at(starting-style) {
  /* declarations */
}
```

becomes:

```css
.fade-in {
  /* declarations */
}

 .fade-out {
  @starting-style {
    & { /* declarations */ }
  }
}
```

----

If the compound selector consists entirely of `:at()` and is using a descendant selector, the `@-rule` is terminal (i.e. the declarations are placed directly within it):


```css
:at(page), .page:at(media screen) {
  /* declarations */
}
```

becomes:

```css
@page {
  /* declarations */
}

@media screen {
  .page {
    /* declarations */
  }
}
```

---

```css
.fade-in,
.fade-out :at(starting-style) {
  /* declarations */
}
```

becomes:

```css
.fade-in {
  /* declarations */
}

 .fade-out {
  @starting-style {
    /* declarations */
  }
}
```

----

```css
.dark,
:at(container style(--dark: 1)) {
  /* declarations */
}
```

becomes:

```css
.dark {
  /* declarations */
}

@container style(--dark: 1) {
  & { 
    /* declarations */ 
  }
}
```

----


While the relative order of `:at()` pseudo-classes and other simple selectors in the same compound selector doesn't make a difference, the _relative_ order of `:at()` pseudo-classes if multiple are present is reflected in the nesting:

```css
.foo:at(media(width < 500px)):at(layer) {
  /* declarations */
}
```

becomes:

```css
@media(width < 500px) { 
  @layer {
    .foo {
      /* declarations */
    }
  }
}
```

-----

Non-descendant combinators are simply applied to the selector that remains after all the `:at()` have been removed:

```css
.fade-in,
.fade-out > :at(starting-style) {
  /* declarations */
}
```

becomes:

```css
.fade-in {
  /* declarations */
}

 .fade-out {
  @starting-style {
    > & { /* declarations */ }
  }
}
```

----

Being purely syntactical, this allows nonsense CSS to be created:

```css
.foo .bar:at(font-face) {
  /* declarations */
}
```

becomes:

```css
.foo {
  @font-face {
    .bar {
      /* declarations */
    }
  }
}
```

That’s okay. Simply writing CSS _also_ allows nonsense CSS to be created 😀 

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/11969 using your GitHub account


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

Received on Wednesday, 19 March 2025 21:58:11 UTC