Skip to content

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.

Previous PostPublishing to My Blog with github.dev