Using :scope Is Pretty Cool
While trying to solve a problem at work, a coleague and I found out about the :scope
pseudo-class. In a nutshell, it allows you to refer to the current element's scope in a CSS selector.
When writing a stylesheet, :scope
is the same thing as :root
, because there is no way to write scoped stylesheets. Scoped stylesheets was a feature that was proposed some time ago, and some browsers experimented with, but have since been removed from browsers and removed from the spec.
Even though we cannot do anything useful with :scope
in our stylesheets, we can use the :scope
pseudo-class with the DOM API!
When using DOM API methods that expect CSS selectors, you can use :scope
to refer to the element from which you are calling it!
element.matches(":scope") === true; // element is itself, so true!
Things You Can Do with :scope
Selecting Direct Descendants
:scope
is particularly useful when selecting direct descendants of the current element that match some criteria!
element.querySelector(":scope > input#username");
An alternative, would be to get all the children of element
and match them against a selector.
Array.from(element.children).find((child) => child.matches("input#username"));
Another approach would be to refer to the current element using an ID or class:
element.setAttribute("id", "myid");
element.querySelector("#myid > input#username");
But this would be a bit harder to maintain, as the selector and whatever attribute we are using to match the current element would need to be in sync.
Assertions That Involve Hierarchy
Another use case for :scope
is to elegantly assert whether the current element matches a particular selector without needing to use an ID or class to refer to the current element.
// element is a direct child of `div#wepa`
element.matches("div#wepa > :scope"); // => `boolean`
// element is descendant of #content
element.matches("#content :scope"); // => `boolean`
// element is the next sibling of a `label`
element.matches("label + :scope"); // => `boolean`
// the next sibling of the element is a `label`
element.matches(":scope + label"); // => `boolean`
// element is a descendant of a `form` element,
// and has a `textarea` as a child
element.matches("form :scope > textarea"); // => `boolean`
Again, there are other ways to do this, but this way is probably more expressive, concise, and easier to maintain.
Limitations
A noteworthy limitation that I've noticed, however, is that using Element.querySelector()
or Element.querySelectorAll()
to match siblings of the current element does not work.
This likely due to the fact that querySelector
and querySelectorAll
only consider the descendants of the current element, so it makes sense.
// try to get all siblings of the current element
element.querySelectorAll(":scope ~ *"); // => Empty NodeList
// try to get the next sibling of the current element
element.querySelectorAll(":scope + *"); // => Empty Nodelist
In that case we still have to use the traditional methods, like getting all the children of the parent element, matching them against a selector, and filtering out our "target element".
Likely for the same reason, :scope
cannot be used to match the current element itself. Which would not be useful, but interesting to know about.
element.querySelector(":scope"); // => null
Browser Support
I expected :scope
to have limited browser support. Surprisingly, at the time of this writing :scope
has very good browser support.
The TL;DR is if you don't have to support Internet Explorer, you can use :scope
!
What's Next?
I hope you enjoyed this quick writeup. I'm going to write a couple more about other interesting Web features that I've found myself using recently.