Selector specificity is not the next step in the cascade, but it’s the cascade feature we talk about the most, and have to think about every time we write a selector.
At this point, most of the styles on the web are author styles.
All of our CSS comes from the author origin, they’re rarely important, exist mostly in the same context, get attached using selectors, and sit in the root layer by default.
So selector specificity is – and always has been – our primary interaction with the cascade.

Specificity is all hype, and it’s time for the bubble to pop.
And I think we talk about it too much!

Like every other cascade heuristic, the default assumption here is that more targeted rules should override more generic rules.
And this time we get to officially call that assumption specificity.
* { box-sizing: border-box; }
The universal selector selects (almost) everything, so it has zero specificity. And then we get three specificity layers on top of that.
*,
::before,
::after {
box-sizing: border-box;
}
But the universal selector doesn’t select pseudo-elements, so you’ll sometimes see this even-more-universal approach.
button, div, span {…}
::before, ::after, ::part() {…}
Element names are pretty generic since the same element might be used in widely different situations.
This group includes pseudo-elements
like ::before and ::after,
::first-line, ::first-letter,
the ::marker on a list-item,
or the ::backdrop on a dialog.
You can find the full list on MDN, and see some new form pseudo-elements are on the way.
.action, .summary {…}
:hover, :user-invalid {…}
[hidden] {…}
Class and attribute selectors are more targeted, since we’re using them to group reusable patterns – but they are still pretty broad and flexible.
This includes pseudo-classes, like hover, focus, active, and visited, or first, last, and nth-child, checked, blank, valid, invalid, user-invalid, and many more.
a[href*="://"] { /* external links */ }
a[href^="https:"] { /* secure links */ }
a[href$=".pdf"] { /* pdf links */ }
button[aria-pressed=true] { /* pressed buttons */ }
img:not([alt]) { /* images without alt text */ }
The attribute syntax gives us a lot of flexibility, with different ways to parse and select based on attribute values. I use these all the time, maybe more than classes.
#call-to-action {…}
And then ID selectors are the most specific selector, because they are supposed to be unique – only matching a single element per page.
* { color: red; }
p { color: orange; }
.fancy { color: blue; }
#page-summary { color: indigo; }
So if we have a cascade conflict between simple selectors in each category, the most specific selector will win. Id’s will override classes and attributes, which override element names, which override the universal selector.
These are called ‘simple selectors’, when they only contain one single piece of information – but selectors don’t have to be simple.
I think of selectors like words and sentences. These are the words that we can string together to express something more nuanced and complex.
form#contact button[type='submit']:active {
border-color: seaGreen;
}
With a complex selector we have to add up how many words are in each layer of specificity. One ID, two attributes, and two elements.
[1,0,2] > [0,3,2]
We can compare these like software versions. Version 1.2.0 is larger than any version starting with a 0.
So one ID is all it takes to override any number of classes. But if we have the same number of IDs and classes, then we have to compare the number of elements.
We never have to compare all the numbers at once, we only care about one column at a time.
And look, I think that’s great to understand in theory –
But don’t waste your time! This is like learning the rules of grammar because you want to write a haiku.
Specificity is just a heuristic. It’s just there to give us a good default, based on the selectors we’re writing. But as soon as we notice it, it’s not the right tool for the job!
If you need a specificity calculator, stop what you’re doing, and reach for layers or scope instead.
I know we haven’t talked about layers or scope yet, but they exist so that you never have to calculate specificity ever again.
This is another of those places where popular conventions will tell you to spend all your energy worrying about specificity.
/* expressive browser-ux */
form:has(:invalid) [type="submit"] { … }
/* we do all the work??? */
.form__btn--submit--invalid { … }
People are taking perfectly good sentences, and trying to shove the whole thing into a single word – just to keep the specificity low.
No wonder everyone’s in a panic about naming conventions! Classes are all part of the same name-space, so only using one per selector, we need some pretty specific names to hit our target!
And then we complain that naming things is hard. But I didn’t name a single thing in the original sentence – it’s all right there in the HTML.
button { … }
.button { … }
#button { … }
[data-style=button] { … }
CSS only has a single global namespace if you limit yourself to class names. IDs have their own space, so do element names, and we can create any namespace we want by using attribute selectors.

This is my hottest take, but I think we should use selectors to select things.
h2:has(> button[aria-expanded="false"]) + div {
display: none;
}
In fact I consider that the core feature – and the most powerful feature – of the language.
We can write some really good meaningful and expressive sentences without inventing new names for anything.
This excellent sentence comes from Adrian Roselli, and is useful for styling disclosure widgets.
.flow > * + * {
margin-block-start: var(--flow-space, 1em);
}
Or you might be familiar with Heydon Pickering’s owl selector, which adds styles to everything but the first element in a set of siblings.
.flow > :not(:first-child) {
margin-block-start: var(--flow-space, 1em);
}
Although these days I think you might be better off using ‘not first-child’ to achieve the same result.
/* doesn't have to be fancy */
- .btn { … }
+ button { … }
And it doesn’t have to be fancy. Craig Abbott has a new article pointing to the issues with making every style work everywhere. That flexibility makes things easier for developers… But often things that should not be easier!
We can use CSS to encourage correct usage of HTML.
I do understand how we got here. Layers and scopes are both fairly new, and without them we had no good way to sidestep specificity conflicts. Our only option was making the heuristic work –
Or jumping all the way to importance. We had to get creative.

Specificity is all hype, and it’s time for the bubble to pop.
But those days are over.
Scope is new, but everywhere.
cascade-layers
And layers have been in every browser since March of 2022.
So let’s look at how they work.
(across large teams & projects)
(through lower boundaries & proximity)
.title { /* global */ }
.post .title { /* nested */ }
.post__title { /* BEM */ }
@scope (<root>) { … }
Always child or descendant


BEM, CSS Modules, Vue, JSX, Stylable, etc
.post__title { /* BEM */ }
.title[data-JKGHJ] { /* Vue */ }
@scope (<root>) to (<boundary>) {…}@scope (.media) to (.content) {
img { /* only images that are "in scope" */ }
}
<article>
<style scoped>
p { color: green; }
</style>
<p>This paragraph will be green.</p>
</article>
<p>This paragraph won't!</p>
<article>
<style>
@scope { p { color: green; } }
</style>
<p>This paragraph will be green.</p>
</article>
<p>This paragraph won't!</p>


@scope (.block-name) to (.block-content) {
.title { /* only inside the block! */ }
}
button {
&:hover { background: hotPink; }
&[aria-pressed=true] { border: thick solid teal; }
@media (width > 30em) { padding-inline: 1em; }
& + & { margin-inline-start: 1ch; }
}
.light-theme a { color: purple; }
.dark-theme a { color: plum; }
@scope (.light-theme) {
a { /* similar to simple nesting… */ }
}
@scope (.dark-theme) {
a { /* but the _closer_ scope root wins… */ }
}


@layer settings { … }
@layer tools { … }
@layer generic { … }
@layer elements { … }
@layer objects { … }
@layer components { … }
@layer overrides { … }
/* establish layer order */
@layer one, two, three;
/* add code to layers as needed */
@import url(two.css) layer(two);
@layer three { … }
@layer one { … }
@layer two { … }
@layer components {
@layer state { … }
}
/* access nested layers */
@layer components.state { … }
/* system.css */
@layer theme { … }
@layer components { … }
@import url(system.css) layer(system);
@layer system.theme { … }
@layer system.components { … }
@import url(system.css) layer(system);
@layer system.theme { … }
@layer system.components { … }
@layer system.custom { … }
Add your own sub-layer to an imported layer!
@import url(bootstrap.css) layer(bootstrap.theirs);
@layer bootstrap.ours {
/* anything here will override bootstrap */
}
@layer components {
@layer defaults, themes, state;
}
my recommendations
/* add as the first styles */
@layer reset, framework, components, utilities;
<style>/* keep this before linked styles */
@layer reset, framework, components, utilities;
</style>
<link rel="stylesheet" href="…">
<link rel="stylesheet" href="…">
<style>
@layer reset, framework, components, utilities;
@import url(…) layer(reset);
@import url(…) layer(framework);
</style>
*Including inter-org dependencies
@import url(bootstrap.css) layer(bootstrap.vendor);
@layer bootstrap.overrides {
/* anything here will override bootstrap */
}





<template>…</template>
<style>
@layer components {
/* … */
}
</style>
*the styles will be hidden from older browsers
Like origins and context Important layers are reversed
!important
And then we add important styles inside each layer

Again, the goal is balance
-> Layers -> Importance
@layer reset {
[hidden] { display: none !important; }
}
Layering and importance are the only two cascade features entirely designed for cascade control – and not as a side-effect of some other feature.
When we run into conflicts, it can be helpful to look at things like source-order, scope, specificity, and attachment, to make sure that they match our intent.
But at the point where we start considering hacks to change specificity or scope, we’d be better off using one of the more explicit tools.
-> Layers -> Importance
And each one serves a different purpose. If we have a low powered style that is essential, and needs protection from future styles – use importance. But if there’s any other reasoning to which style should win, layers are likely the best tool for the job.
Cascade conflicts are inevitable. And when they resolve in unexpected ways, it’s our job to clarify what should take priority. Layers provide that clarity in a way that nothing else can.