Module 2: Selectors & Combinators
Specificity and the Cascade: How the Browser Picks a Winner
The exact order the browser uses to resolve conflicting declarations, how specificity is calculated, how inheritance works, and a method for debugging rules that will not apply.
When two rules set the same property on the same element, only one value can win. The browser does not guess. It runs a fixed sequence of tie-breakers called the cascade, and every "my CSS is not working" bug is a cascade bug in disguise.
The cascade in order#
For each property on each element, the browser collects every declaration that matches, then sorts them using these steps. It stops at the first step that gives a winner.
1. Origin and importance !important beats normal; author beats user-agent
2. Inline styles the style="" attribute beats stylesheet rules
3. Cascade layers later layers beat earlier ones (unlayered beats layered)
4. Specificity how targeted is the selector?
5. Order of appearance the last declaration wins
Most day-to-day conflicts get decided at step 4 or 5. Knowing steps 1 to 3 explains the strange ones.
Calculating specificity#
Specificity is three counters, written as a tuple:
(ids, classes, types)
- IDs: each
#idadds to the first counter. - Classes: each
.class,[attribute]and:pseudo-classadds to the second. - Types: each element name and
::pseudo-elementadds to the third. - The universal selector
*and combinators add nothing.
p /* (0,0,1) */
.intro /* (0,1,0) */
article .intro /* (0,1,1) */
p.intro:hover /* (0,2,1) */
#hero .intro /* (1,1,0) */
#hero #title /* (2,0,0) */
Compare tuples left to right. The first counter that differs decides. The counters do not carry over into each other, so even 20 classes (0,20,0) lose to a single ID (1,0,0). This is not decimal arithmetic.
.intro { color: green; } /* (0,1,0) */
#hero { color: red; } /* (1,0,0) wins, no matter where it is written */
Inline styles sit above all of this, as if they were a fourth counter to the left, (1,0,0,0).
Specificity of the modern pseudo-classes#
:is()and:not()take the specificity of their most specific argument.:is(.a, #b)is(1,0,0), even if only.amatched.:has()works the same way.:where()is always(0,0,0).
That last one is genuinely useful. Base styles written inside :where() are trivial to override:
:where(.prose) a { color: #0b5fff; } /* specificity (0,0,1) */
.footer a { color: white; } /* (0,1,1), wins without a fight */
Order of appearance#
When specificity ties, the declaration that comes last in the source wins. That includes across files, in the order they load.
.btn { background: blue; }
.btn { background: green; } /* green */
Combine that with specificity and you can predict nearly everything:
.card p { color: gray; } /* (0,1,1) */
p.lead { color: black; } /* (0,1,1) - tie, so the later rule wins */
Importance and origin#
!important flips the normal order. An important declaration beats any non-important one, regardless of specificity.
.btn { background: blue !important; }
#nav .btn { background: red; } /* loses, even with higher specificity */
Two important declarations fight each other by the usual rules, so using !important once means you will need it again to override it. Treat it as a last resort, and a smell.
Origin means where the style came from: the browser's built-in stylesheet, a user's own settings, or your stylesheet. Your author styles beat the browser defaults, which is why your CSS works at all. For !important, the order inverts, so that a user's important accessibility settings beat the page.
Cascade layers#
Layers let you set the priority of whole groups of styles, without fighting specificity.
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; }
}
@layer components {
.btn { padding: 0.5rem 1rem; }
}
@layer utilities {
.p-0 { padding: 0; } /* beats .btn, even though specificity is equal or lower */
}
Layers declared later beat earlier ones, regardless of selector specificity. Unlayered styles beat all layered styles. This is how modern projects keep a third-party library from overriding their own code: put the library in a low layer.
Inheritance#
Some properties flow down from parent to child by default, without any rule targeting the child.
- Inherited:
color,font-family,font-size,line-height,text-align,visibility,cursor, list styles. - Not inherited:
margin,padding,border,background,width,height,display,position.
That split makes sense. If border were inherited, putting a border on a ul would put one on every li too.
Inherited values have the lowest priority of all. Any rule that targets the element directly beats an inherited value, even a type selector like a { color: blue; }. That is why a link will not pick up its parent's color unless you tell it to.
You can control it with keywords that work on any property:
a { color: inherit; } /* take the parent's value */
.box { border: initial; } /* reset to the spec's default */
.reset { all: unset; } /* wipe everything: inherit if inherited, initial if not */
.x { color: revert; } /* roll back to the browser default */
A debugging method that works#
When a rule does not apply, do not change the CSS yet. Check these in order.
- Is the selector matching? In DevTools, select the element and look at the Styles pane. If your rule is not listed at all, the selector is wrong or the file did not load.
- Is the declaration valid? A strikethrough with a warning icon means the property or value is invalid.
- Is it overridden? A normal strikethrough means a rule with higher priority won. The winner is listed above it.
- Why did the other rule win? Compare specificity first, then order, then layers and
!important. - Fix the cause. Make your selector more specific in a deliberate way, or simplify the competing selector.
Keeping specificity under control#
- Style with single classes where possible. Flat specificity means order decides, which is predictable.
- Avoid IDs for styling and avoid chains longer than three parts.
- Do not use
!importantto win a fight. Use it only for utilities that must always apply, like a.hiddenclass. - Use
:where()and@layerfor defaults that should be easy to override.
Common mistakes#
- Thinking the later rule always wins. Only when specificity ties.
- Adding an ID to "fix" something, which raises the specificity ceiling for everyone after you.
- Counting classes as base-10 digits. Eleven classes do not beat one ID.
- Forgetting that inline styles beat selectors.
- Expecting a property to inherit when it does not (such as
borderon a child).
Practice#
- Write a table of these selectors with their specificity tuples and sort them from weakest to strongest:
ul li a,#nav a:hover,.menu .item.active,a,:where(nav) a. - Create two rules that conflict on
colorand make each win in turn using only source order. Then make the first win using specificity without changing the order. - Put a library stylesheet in a low
@layerand write your override with a simple class selector. Confirm that it wins. - Intentionally build a conflict that involves
!important, then remove the!importantby restructuring the selectors.