The usual advice is to dump everything into :root and move on. That works, but there are several ways to select html in CSS, and the one you pick decides how hard those styles are to override later.
I have spent the last decade cleaning up “legacy” CSS in custom WordPress themes, and two problems keep coming back: selectors too weak to win anything, and selectors so strong that the only way past them is !important. Both usually start at the root.
1. The basic html selector
The plain element selector has a specificity of 0-0-1. It works everywhere and it is easy to override, which is exactly what you want from base styles.
html {
background-color: #f0f0f0;
scroll-behavior: smooth;
}
2. The :root pseudo-class
The MDN documentation describes :root as the <html> element in an HTML document. The catch is specificity: 0-1-0, which puts it above the bare html selector.
This is where most of us declare global CSS variables. Because :root is a pseudo-class, it beats a plain html block written somewhere else in the sheet. On large projects that becomes a real source of confusion, with the reset file and the variables file quietly fighting over the same properties.
:root {
--primary-color: #007cba;
--main-font: 'Inter', sans-serif;
}
3. The modern :scope selector
:scope comes out of the W3C Selectors Level 4 spec. Used at the top level of a stylesheet, it matches the <html> element.
I reach for :scope on global variables because it reads like what it is: these belong to the global scope. In a browser the behavior comes out identical to :root, so treat this as a preference rather than a technique.
For scoping past the root element, I wrote a longer piece on using CSS @scope instead of heavy abstractions in WordPress projects.
4. The nesting ampersand (&)
Native CSS nesting is baseline now, which makes & at the top level another way to select html in CSS. Outside any parent block, & falls back to the scope root, and that is the HTML element. Developers coming from SCSS trip over this constantly.
5. Quirky structural selectors
Then there are the war story selectors, the ones you reach for when the markup is out of your hands or you are feeling clever. :has(body) and :has(head) can only ever match the root element. That is a curiosity, not something you write on purpose.
My favorite of the useless ones is the “bird” selector:
:not(* > *) {
/* This selects the root because it's the only element
that is NOT a child of something else. */
}
It works because <html> is the only node without a parent. Just because you can write it does not mean you should ship it. Use :root and save your coworkers the puzzle.
Specificity in WordPress is its own mess. If block styles keep overriding your theme, I covered how the WordPress CSS specificity fix works in a separate post.
If this kind of CSS cleanup is eating your dev hours, I can take it off your plate. I have been working with WordPress since the 4.x days.
What to actually use
Use :root for variables and html for base typography and background resets. Skip the structural hacks unless you are writing a specialized CSS scraper or dealing with a genuinely broken document tree. Keep specificity low. Boring is what you want when someone else has to override your theme.