[csswg-drafts] [selectors] Syntax for matching attribute value relationships (combinator? @-rule? Something else?) (#12446)

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

== [selectors] Syntax for matching attribute value relationships (combinator? @-rule? Something else?) ==
> [!NOTE]
> In the course of writing this, I became convinced that the "backreference" syntax proposed in #10567, if implementable, is much better in terms of ergonomics. I’m posting this anyway, in case #10567 is deemed too hard to implement.

In #10970 I proposed a generic `/idref()/` combinator to address the numerous use cases where we want to go from an IDREF to the element it is referencing:

- `for`, in `<label>` and `<output>`
- `list` in `<input>`
- A host of ARIA attributes (e.g. `aria-describedby`, `aria-labelledby`, `aria-activedescendant`, `aria-controls`, `aria-details`, `aria-flowto`, `aria-owns` etc.)
- Popovers (`popovertarget`)
- Invokers (`invoketarget`)
- [`anchor`](https://github.com/whatwg/html/pull/9144)
- Plus, web component authors can always define their own, custom IDREF attributes

This resurfaced recently due to invokers (see #12436).
While **idrefs are definitely the majority use case**, #10567 argues that there are enough use cases that are not idrefs and thus a more generic solution could be useful.
Perhaps instead of embedding the source attribute in the combinator name (`idref`), we could use a more generic name (`ref`? `attref`?) where the source attribute is a parameter, defaulting to `id`. Then, initial implementations could ship without this parameter, and add it later.

One use case that comes to mind is reversing idref relationships. For example getting all popover invokers that target a given popover (`/attref(id = popovertarget)/`).

There are also many interactive widgets that with this could be implemented via form elements + CSS.
For example, one could basically implement tabs like this (with suitable styling — yes, selects can be styled to be horizontal):
```html
<select size="4" aria-orientation="horizontal" class="tab-bar">
 <option value="foo">Foo</option>
 <option value="bar">Bar</option>
</select>
<div class="tab-panel" data-panel="foo"><!-- Foo content --></div>
<div class="tab-panel" data-panel="bar"><!-- Bar content --></div>
```

Then `.tab-bar > option:checked /attref(data-panel = value)/ .tab-panel` would target the active panel.

Custom referencing mechanisms like [this one](https://github.com/w3c/csswg-drafts/issues/10567#issuecomment-2227409901) could also be implemented that way.

Similarly, we could plan ahead for expanding how matching happens down the line beyond `=`. For example, several ARIA attributes take multiple ids, e.g. `aria-activedescendant`, `aria-controls`, `aria-describedby`, `aria-details`, `aria-errormessage`, `aria-flowto`, `aria-labelledby`, `aria-owns`, but once `~=` is allowed their targets can be targeted via e.g. `/attref(id ~= aria-labelledby)/`

Since the attribute name is arbitrary, this also means people can use this to match the **same source and target attributes**, as long as the combinator is defined to always exclude the element it starts from.
For example, if a `<foo-callout>` web component has a `variant` attribute with values like `brand | neutral | danger | warning` it could match children that have the same attribute as the parent with `foo-callout /attr(variant = variant)/ *:is(foo-callout[variant] *)

## Pros & Cons

Pros:
- The matching step is very explicit, and very constrained, making it potentially easier to implement
- For simple idref queries, it can be simpler than the backreference proposal (which would require expanding the backreference scope to selector lists since there is no suitable combinator)

Cons:
- **Order**: it's hard to remember what is the order of attribute matched vs source attribute.
- **Naming**: `ref()` is too generic, `attref()` is awkward (is it `attref` or `attrref`?) and `attr()` sounds like the existing css-values function.
- **Consistency**: we're basically having something _like_ an attribute selector, that is not quite an attribute selector.
- It may still make sense to deploy `idref()` separately, because otherwise we'd need to make `attref(foo)` resolve to `attref(id = foo)` to cover idrefs with the MVP which is not necessarily a good default. A better default might be to expand to `attref(foo = foo)`, since duplicating attribute names comes up and is pretty awkward.
- **Ergonomics**. Some of the use cases that are trivial with a backreference syntax are complex here. The backreference syntax especially shines where you want to combine the matching step with a combinator (e.g. "find children with the same attribute"). With this, you'd need to use `:is()` and filter the target to apply the additional combinator (see tab and callout examples above). 

## Alternative proposal

A new @-rule  e.g. `@attr` that takes an attribute name and an optional selector.

The tab example becomes:
```css
@attr value (.tab-bar > option:checked) {
 .tab-panel[data-panel=attr(value)] { 
 
 }
}
```

```css
@attr variant (foo-callout) {
 [variant=attr(variant)] {
  
 }
}
```

Pros:
- Similar flexibility as the backreference idea, while potentially reducing implementation complexity.
- No need to introduce new syntax, regular attribute selectors work fine for the matching
- Easier when we also want to match a regular combinator relationship *in addition* to the attribute value (e.g. "elements *inside* `<foo-callout>` with the same variant")
- Can be nested to match multiple attributes on different selectors

Cons:
- Because it's no longer a selector, it cannot be used in `querySelectorAll` and any other context that takes a selector

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


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

Received on Thursday, 3 July 2025 20:53:53 UTC