-
Notifications
You must be signed in to change notification settings - Fork 756
Description
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>listin<input>- A host of ARIA attributes (e.g.
aria-describedby,aria-labelledby,aria-activedescendant,aria-controls,aria-details,aria-flowto,aria-ownsetc.) - Popovers (
popovertarget) - Invokers (
invoketarget) anchor- 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):
<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 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 itattreforattrref?) andattr()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 makeattref(foo)resolve toattref(id = foo)to cover idrefs with the MVP which is not necessarily a good default. A better default might be to expand toattref(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:
@attr value (.tab-bar > option:checked) {
.tab-panel[data-panel=attr(value)] {
}
}@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
querySelectorAlland any other context that takes a selector