The Different Ways to Select in CSS
Temani Afif recently did this exercise and I thought I’d build off of it. Some of these are useful. Many of them are not. There’s a bird at the end!
html
html {
/* I mean, duh */
}
:root
:root {
/* Sarsaparilla, anyone? */
}
:root is a CSS pseudo-class that matches the root element of the current (XML) document. If the current document is a HTML document, then it matches <html>. The XML documents that you’ll most likely encounter as a web developer (besides HTML) are:
- SVG documents:
:rootmatches<svg> - RSS documents:
:rootmatches<rss> - Atom documents:
:rootmatches<feed> - MathML documents:
:rootmatches<math> - Other XML documents:
:rootmatches the outermost element (e.g.,<note>)
But what’s the practicality of :root? Well, the specificity of pseudo-classes (0-1-0) is higher than that of elements (0-0-1), so you’re less likely to run into conflicts with :root.
It’s conventional to declare global custom properties on :root, but I actually prefer :scope because it semantically matches the global scope. In practice though, it makes no difference.
/* Global variables */
:root { --color: black; }
:scope { --color: black; }
Let’s talk about :scope some more…
:scope or &
:scope {
/* Insert scope creep here */
}
Okay, that’s not really what :scope is for.
As I mentioned, :scope matches the global scope root (<html>). However, this is only true when not used within the newly baseline @scope at-rule, which is used to define a custom scope root.
We can also do this:
& {
/* And...? */
}
Normally, the & selector is used with CSS nesting to concatenate the current selector to the containing selector, enabling us to nest selectors even when we aren’t technically dealing with nested selectors. For example:
element:hover {
/* This */
}
element {
&:hover {
/* Becomes this (notice the &) */
}
}
element {
:hover {
/* Because this (with no &) */
}
}
element :hover {
/* Means this (notice the space before :hover) */
}
element {
:hover & {
/* Means :hover element, but I digress */
}
}
When & isn’t nested, it simply selects the scope root, which outside of an @scope block is <html>. Who knew?
:has(head) or :has(body)
:has(head) {
/* Nice! */
}
:has(body) {
/* Even better! */
}
<html> elements should only contain a <head> and <body> (à la Anakin Skywalker) as direct children. Any other markup inserted here is invalid, although parsers will typically move it into the <head> or <body> anyway. More importantly, no other element is allowed to contain <head> or <body>, so when we say :has(head) or :has(body), this can only refer to the <html> element, unless you mistakenly insert <head> or <body> inside of <head> or <body>. But why would you? That’s just nasty.
Is :has(head) or :has(body) practical? No. But I am going to plug :has(), and you also learned about the illegal things that you shouldn’t do to HTML bodies.
:not(* *)
:not(* *) {
/* (* *) are my starry eyes looking at CSS <3 */
}
Any element that’s contained by another element (* *)? Yeah, :not() that. The only element that’s not contained by another element is the <html> element. *, by the way, is called the universal selector.
And if you throw a child combinator right in the middle of them, you get a cute bird:
:not(* > *) {
/* Chirp, chirp */
}
“Siri, file this under Completely Useless.” (Ironically, Siri did no such thing).
The Different Ways to Select <html> in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
This post first appeared on Read More

