Selectors and the Cascade
Two rules set the same property on the same element. One of them wins. CSS is completely deterministic about which - it is only mysterious until you know the order the browser checks things in.
This lesson is that order, and what to do instead of reaching for !important.
The order, in full
When two declarations conflict, the browser works through these in sequence and stops at the first one that separates them.
- Origin and importance. Your stylesheet beats the browser's defaults. An
!importantdeclaration beats a normal one - but a user's!importantbeats yours, which is deliberate: somebody who needs a 24-pixel minimum font size must be able to have one. - Cascade layers. Later layers win over earlier ones, regardless of specificity. Covered properly in the Level 4 lesson.
- Specificity. The more specific selector wins.
- Source order. The one written later wins.
Most day-to-day confusion is steps three and four, and most of it comes from misreading step three.
Specificity is three numbers, not one
Count three things about a selector:
- A - how many id selectors it has
- B - how many class, attribute and pseudo-class selectors
- C - how many element and pseudo-element selectors
Write it as A-B-C and compare left to right. A higher A wins outright, whatever B and C are.
a /* 0-0-1 */
.nav a /* 0-1-1 */
nav ul li a /* 0-0-4 */
.nav .list .item a /* 0-3-1 */
#header a /* 1-0-1 */
The important consequence: .nav a at 0-1-1 beats nav ul li a at 0-0-4, even though the second is four selectors long and looks more specific. One class beats any number of elements. This is the single most common misreading, and it is why adding more descendant selectors to "win" an argument usually does nothing.
Three things do not count at all:
* { } /* 0-0-0 - the universal selector is free */
:where(.a, .b) { } /* 0-0-0 - :where always contributes nothing */
:is(.a, #b) { } /* takes the HIGHEST of its arguments: 1-0-0 */
:not(.a) { } /* takes its argument: 0-1-0 */
:where() is genuinely useful because of this. A reset written with it can be overridden by a single class without a fight:
:where(h1, h2, h3) { margin-block: 0 .5em; } /* 0-0-0 */
.card h2 { margin: 0; } /* wins easily */
Inline styles and the one thing above them
<p style="color: red">...</p>
Open this example in the editor and change it. Nothing you type is saved.
An inline style sits above every selector - think of it as 1-0-0-0. The only normal declaration that can beat it is... none. The only thing that can is !important in a stylesheet.
That is the honest reason !important exists, and it is why a codebase that uses inline styles ends up using !important, and then needs a second !important to beat the first.
Source order, and why it matters more than people think
When specificity ties, the later rule wins. Simple, and it is the whole mechanism behind a utility class being overridden by one written after it, and behind the order of your @import statements mattering.
.button { background: grey; }
.button { background: blue; } /* this one */
This is also why a stylesheet's order is architecture rather than housekeeping - which is exactly the problem cascade layers were introduced to solve.
Inheritance is a different mechanism
Specificity decides between rules that match an element. Inheritance is what happens when no rule matches: some properties take their value from the parent.
body { color: #333; font-family: Inter, sans-serif; }
/* every descendant gets both, with no selector at all */
Inherited by default: color, font-*, line-height, text-align, visibility, cursor, list-style.
Not inherited: background, border, padding, margin, width, display, position.
A directly matching rule of any specificity always beats an inherited value, however specific the parent's selector was. An inherited value is not a competitor in the cascade; it is what happens in its absence.
Four keywords let you ask for it explicitly:
.thing {
color: inherit; /* take the parent's value */
border: initial; /* the property's own default */
padding: unset; /* inherit if inheritable, initial if not */
margin: revert; /* back to the browser's own stylesheet */
}
inherit on a form control is the most useful of these in practice - inputs and buttons do not inherit font by default, which is why they look like the operating system until you say otherwise:
input, button, select, textarea { font: inherit; }
The selectors worth knowing
/* Combinators */
.card p { } /* any p inside .card, at any depth */
.card > p { } /* only a direct child */
h2 + p { } /* the p immediately after an h2 */
h2 ~ p { } /* every p after an h2, same parent */
/* Attributes */
[disabled] { }
[type="email"] { }
[href^="https://"] { } /* starts with */
[href$=".pdf"] { } /* ends with */
[class*="col-"] { } /* contains */
/* Structure */
li:first-child { }
li:last-child { }
li:nth-child(2n) { } /* even */
li:nth-child(3n+1) { } /* 1st, 4th, 7th */
li:only-child { }
p:empty { }
/* State */
a:hover, a:focus-visible { }
input:checked { }
input:user-invalid { }
details[open] { }
/* Relational - the parent selector CSS lacked for twenty years */
.card:has(img) { } /* a card that contains an image */
label:has(input:checked) { }
:has() is worth dwelling on. It is the first selector that looks down the tree, which makes a whole category of problem solvable in CSS that previously needed JavaScript:
/* A form field whose input is invalid */
.field:has(input:user-invalid) { border-color: var(--danger); }
/* A layout that changes when there is a sidebar */
.layout:has(> aside) { grid-template-columns: 1fr 18rem; }
!important, and what to do instead
Reaching for !important is almost always a sign that the stylesheet's order or structure is wrong. It wins the argument and makes the next one worse, because the only way to beat it is another !important.
The usual causes and their real fixes:
| Why you reached for it | What to do instead |
|---|---|
| A third-party stylesheet is winning | Put it in an earlier cascade layer |
| An inline style from a script | Remove the inline style; use a class |
| A selector elsewhere is too specific | Lower that selector, do not raise this one |
| You cannot find what is winning | Open devtools; it shows you, with the loser struck through |
There is one honest use: a utility class that must always win, in a system where that is the documented contract.
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip-path: inset(50%) !important;
}
Debugging, which takes thirty seconds
Open the element inspector. The Styles panel lists every rule that matched, in cascade order, with the losing declarations struck through, and the file and line each came from. The Computed tab shows the final value and lets you expand it to see which rule supplied it.
You never have to reason about specificity from first principles. You have to know that the order exists so that you know what you are looking at - and then the browser tells you the rest.
What goes wrong
| Symptom | Cause |
|---|---|
| Adding more selectors changes nothing | One class beats any number of elements |
| A style works, then stops after a refactor | Source order changed |
!important needed to override !important |
Inline styles, or a specificity war |
| Buttons look like the operating system | Form controls do not inherit font |
| A reset is impossible to override | Written without :where() |
| A rule matches but has no effect | Another rule wins - devtools shows which |
The habit
Keep specificity low and flat. One class per rule wherever you can, :where() for resets, layers for order, and devtools open whenever something surprises you. A stylesheet where every rule is 0-1-0 has no specificity problems at all, because source order is the only thing left to decide - and source order is something you control.