You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.)
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):
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 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:
@attr value (.tab-bar>option:checked) {
.tab-panel[data-panel=attr(value)] {
}
}
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
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>aria-describedby,aria-labelledby,aria-activedescendant,aria-controls,aria-details,aria-flowto,aria-ownsetc.)popovertarget)invoketarget)anchorThis 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 toid. 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):
Then
.tab-bar > option:checked /attref(data-panel = value)/ .tab-panelwould 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 avariantattribute with values likebrand | neutral | danger | warningit could match children that have the same attribute as the parent with `foo-callout /attr(variant = variant)/ *:is(foo-callout[variant] *)Pros & Cons
Pros:
Cons:
ref()is too generic,attref()is awkward (is itattreforattrref?) andattr()sounds like the existing css-values function.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.:is()and filter the target to apply the additional combinator (see tab and callout examples above).Alternative proposal
A new @-rule e.g.
@attrthat takes an attribute name and an optional selector.The tab example becomes:
Pros:
<foo-callout>with the same variant")Cons:
querySelectorAlland any other context that takes a selector