Selecting the root element: more than one way to skin <html>
Temani Afif recently explored the many selectors that match the HTML root element. Some are practical tools you might reach for daily. Others are purely academic exercises. Let's walk through them.
The obvious selectors
The most straightforward way to target the root element is the type selector:
html {
/* I mean, duh */
}
But there's also the :root pseudo-class, which matches the root element of whatever XML document is currently being rendered:
:root {
/* Sarsaparilla, anyone? */
}
For an HTML document, :root resolves to <html>. But the same pseudo-class works in any XML context, and since pseudo-classes carry a higher specificity than type selectors, :root can help avoid style collisions.
It's common practice to define global custom properties on :root, though some may prefer :scope for its semantic clarity with global scope. In practice, the choice makes no difference.
/* Global variables */
:root { --color: black; }
:scope { --color: black; }
Working with :scope and &
Outside of an @scope block, :scope matches the global scope root — typically <html>. The same applies when you use the & nesting selector in a non-nested context:
& {
/* And...? */
}
Normally, & is used inside CSS nesting to concatenate with the current selector. When used outside of nesting, it simply stands in for the scope root:
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 */
}
}
:scope {
/* Insert scope creep here */
}
A :has() workaround
An HTML document's <html> element should have only a <head> and a <body> as children. Crucially, no other element is allowed to contain either of those two, which makes :has(head) and :has(body) unambiguous indicators of the root:
:has(head) {
/* Nice! */
}
:has(body) {
/* Even better! */
}
Practically speaking, this isn't a selector you'll need — but it does illustrate both :has() and why invalid HTML nesting is worth avoiding.
The negative check
Any element contained by another element matches * *. Negate it with :not() and you're left with only the top-level element:
:not(* *) {
/* (* *) are my starry eyes looking at CSS <3 */
}
And if you throw a child combinator into that same structure:
:not(* > *) {
/* Chirp, chirp */
}


