:has()
As described in the Selectors Level 4 spec, a selector represents a particular pattern of element(s) in a tree structure. We can select specific elements in a tree structure by matching the pattern to the tree.
Generally, this pattern involves two disctinct concepts: First, a means to express conditions to be tested on an element itself (simple selectors or compound selector). Second, a means to express conditions on the relationship between two elements (combinators).
And the subject of a selector is any element matched by the selector.
When you have a reference element in a DOM tree, you can select other elements with a CSS selector.
In a generic tree structure, an element can have 4-way relationships to other elements.
CSS Selectors, to date, have only allowed the last 2 (‘is a next sibling of’ and ‘is a descendant of’).
So in the CSS world, Thor can say “I am Thor, son of Odin” like this: Odin > Thor
. But there has been no way for Darth Vader to tell Luke, “I’m your father”.
At least, these are the limits of what has been implemented and is shipping in every browser to date. However, :has()
in the CSS Selectors spec provides the expression: DarthVader:has(> Luke)
The primary use of selectors has always been in CSS itself. Pages often have 500-2000 CSS rules and slightly more elements in them. Selectors act as filters in the process of applying style rules to elements. If we have 2000 css rules for 2000 elements, matching could be done at least 2,000 times, and in the worst case (in theory) 4,000,000 times. In the browser, the tree is changing constantly - even a static document is rapidly mutated (built) as it is parsed - and we try to render all of this incrementally and at 60 fps. In summary, the selector matching is performed very frequently in performance-critical processes. So, it must be designed and implemented to meet very high performance. And one of the efficient ways to make it is to make the problem simple by limiting complex problems.
In the tree structure, checking a descendant relationship is more efficient than checking an ancestor relationship because an element has only one parent, but it can have multiple children.
<div id=parent>
<div id=subject>
<div id=child1></div>
<div id=child2></div>
...
<div id=child10></div>
</div>
</div>
<script>
subject.matches('#parent > :scope');
// matches : Are you a child of #parent ?
// #subject : Yes, my parent is #parent.
subject.matches(':has(> #child10)');
// matches : Are you a parent of #child10 ?
// #subject : Wait a second, I have to lookup all my children.
// Yes, #child10 is one of my children.
</script>
By removing one of the two opposite directions, we can always place the subject of a selector to the right, no matter how complex the selector is.
ancestor subject
subject
is a descendant of ancestor
”previous_sibling ~ subject
subject
is a next sibling of previous_sibling
”previous_sibling ~ ancestor subject
subject
is a descendant of ancestor
, which is a next sibling of previous_sibling
”With this limitation, we can get the advantages of having simple data structures and simple matching sequences.
<style>
A > B + C { color: red; }
</style>
<!--
'A > B + C' can be parsed as a list of selector/combinator pair.
[
{selector: 'C', combinator: '+'},
{selector: 'B', combinator: '>'},
{selector: 'A', combinator: null}
]
-->
<A> <!-- 3. match 'A' and apply style to C if matched-->
<B></B> <!-- 2. match 'B' and move to parent if matched-->
<C></C> <!-- 1. match 'C' and move to previous if matched-->
</A>
:has()
allows you to select subjects at any positionWith combinators, we can only select downward (descendants, next siblings or descendants of next siblings) from a reference element. But there are many other elements that we can select if the other two relationships, ancestors and previous siblings, are supported.
<div> <!-- ? -->
<div></div> <!-- ? -->
</div>
<div> <!-- ? -->
<div> <!-- ? -->
<div></div> <!-- ? -->
</div>
<div id=reference> <!-- #reference -->
<div></div> <!-- #reference > div -->
</div>
<div> <!-- reference + div -->
<div></div> <!-- reference + div > div -->
</div>
</div>
<div> <!-- ? -->
<div></div> <!-- ? -->
</div>
:has()
provides the way of selecting upward (ancestors, previous siblings, previous siblings of ancestors) from a reference element.
<div> <!-- div:has(+ div > #reference) -->
<div></div> <!-- ? -->
</div>
<div> <!-- div:has(> #reference) -->
<div> <!-- div:has(+ #reference) -->
<div></div> <!-- ? -->
</div>
<div id=reference> <!-- #reference -->
<div></div> <!-- #reference > div -->
</div>
<div> <!-- #reference + div -->
<div></div> <!-- #reference + div > div -->
</div>
</div>
<div> <!-- ? -->
<div></div> <!-- ? -->
</div>
And with some simple combinations, we can select all elements around the reference element.
<div> <!-- div:has(+ div > #reference) -->
<div></div> <!-- div:has(+ div > #reference) > div -->
</div>
<div> <!-- div:has(> #reference) -->
<div> <!-- div:has(+ #reference) -->
<div></div> <!-- div:has(+ #reference) > div -->
</div>
<div id=reference> <!-- #reference -->
<div></div> <!-- #reference > div -->
</div>
<div> <!-- #reference + div -->
<div></div> <!-- #reference + div > div -->
</div>
</div>
<div> <!-- div:has(> #reference) + div -->
<div></div> <!-- div:has(> #reference) + div > div -->
</div>
:has()
?As you might already know, this pseudo class has been delayed for a long time despite the constant interest.
There are many complex situations that makes things difficult when we try to support :has()
.
In this context, :has()
provides the other two relationships (is a parent of, is a previous sibling of), and problems and concerns start from this.
When we meet a complex and difficult problem, the first strategy we can take is to break it down into smaller ones. For :has()
, we can divide the problems with the CSS selector profiles
:has()
matching operation:has()
style invalidation:has()
matching operation:has()
matching operation basically implies descendant lookup overhead as described previously. This is an unavoidable overhead we have to take on when we want to use :has()
functionality.
In some cases, :has()
matching can be O(n2) because of the duplicated argument matching operations. When we call document.querySelectorAll('A:has(B)')
on the DOM <A><A><A><A><A><A><A><A><A><A><B>
, there can be unnecessary argument selector matching because the descendant traversal can occur for every element A
. If so, the number of argument matching operation can be 55(=10+9+8+7+6+5+4+3+2+1) without any optimization, whereas 10 is optimal for this case.
There can be more complex cases involving shadow tree boundary crossing.
:has()
Style invalidationIn a nutshell, the style engine tries to invalidate styles of elements that are possibly affected by a DOM mutation. It has long been designed and highly optimized based on the assumption that, any possibly affected element is the changed element itself or is downward from it.
<style>
.mutation .subject { color: red; }
</style>
<div> <!-- classList.toggle('mutation') affect .subject -->
<div class="subject"></div> <!-- .subject is in downward -->
</div>
But :has()
invalidation is different because the possibly affected element is upward of the changed element (an ancestor, rather than a descendant).
<style>
.subject:has(.mutation) { color: red; }
</style>
<div class="subject"> <!-- .subject is in upward -->
<div></div> <!-- classList.toggle('mutation') affect .subject -->
</div>
In some cases, a change can affect elements in both the upward and downward directions.
<style>
.subject1:has(:is(.mutation1 .something)) { color: red; }
.something:has(.mutation2) .subject2 { color: red; }
</style>
<div class="subject1"> <!-- .subject1 is in upward -->
<div> <!-- classList.toggle('mutation1') affect .subject1 -->
<div class="subject1"> <!-- .subject1 is in downward -->
<div class="something"></div>
</div>
</div>
</div>
<div class="something">
<div class="subject2"> <!-- .subject2 is in upward -->
<div> <!-- classList.toggle('mutation2') affect .subject2 -->
<div class="subject2"></div><!-- .subject2 is in downward -->
</div>
</div>
</div>
Actually, a change can affect everywhere.
<style>
:has(~ .mutation) .subject { color: red; }
:has(.mutation) ~ .subject { color: red; }
</style>
<div>
<div>
<div class="subject"> <!-- not in upward or downward -->
</div>
</div>
<div></div> <!-- classList.toggle('mutation') affect .subject -->
</div>
<div class="subject"></div> <!-- not in upward or downward -->
The expansion of the invalidation traversal scope (from the downward sub-tree to the entire tree) can cause performance degradation. And the violation of the basic assumptions of the invalidation logic (finding a subject from the entire tree instead of finding it from downward) can cause performance degradation and can increase implementation complexity or maintenance overhead, because it will be hard or impossible for the existing invalidation logic to support :has()
invalidation as it is.
(There are many more details about :has()
invalidation, and those will be covered later.)
:has()
?Thanks to funding from eye/o, the :has()
prototyping in the Chromium project was started by Igalia after some investigations.
(You can get rich background about this from the post - “Can I :has()
” by Brian Kardell.)
Prototyping is still underway, but here is our progress so far.
:has()
selector matching (3 CLs):has()
in snapshot profile (1 CL):has()
in snapshot profile
For about the :has()
in snapshot profile, as of now, Chrome Dev (Version 94 released at Aug 19) supports all the :has()
functionalities except some cases involving shadow tree boundary crossing.
You can try :has()
with javascript APIs (querySelectorAll
, querySelector
, matches
, closest
) in snapshot profile after enabling the runtime flag : enable-experimental-web-platform-features.
You can also enable it with the commandline flag : CSSPseudoHasInSnapshotProfile
.
$ google-chrome-unstable \
--enable-blink-features=CSSPseudoHasInSnapshotProfile
:has()
in both (snapshot/live) profile
You can enable :has()
in both profiles with the commandline flag : CSSPseudoHas
.
$ google-chrome-unstable --enable-blink-features=CSSPseudoHas
Support for :has()
in the live profile is still in progress. When you enable :has()
with this flag, you can see that style rules with :has()
are working only at loading time. The style will not be recalculated after DOM changes.