- From: Lea Verou via GitHub <noreply@w3.org>
- Date: Thu, 03 Jul 2025 20:53:53 +0000
- To: public-css-archive@w3.org
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