Selector matching and the cascade
To build the render tree the browser must decide, for every element, which of thousands of CSS rules apply and which one wins, and it does that by matching selectors right to left for speed and then resolving conflicts through the cascade's origin, specificity, and source-order rules.
Selector matching and the cascade
The flagship lesson left the render-tree step as a black box: walk the DOM, and for each visible node work out "which CSS rules match it and what its final style values are." That sentence hides two separate, non-trivial jobs. First the browser has to find every rule that applies to a given element — the matching problem. Then, when more than one rule sets the same property, it has to pick a single winner — the cascade. This lesson is the first job and the tie-breaking rules; lesson 5 picks up right after a winner is chosen and follows that value the rest of the way to something layout can use.
Neither job is done the way you'd probably guess from reading CSS top to bottom.
The matching problem
A real stylesheet — or three, plus a component library, plus a CSS reset — can carry thousands of rules. The DOM can have thousands of elements. Naively, checking "does this rule match this element" for every rule against every element is a lot of comparisons, and browsers do this on every element the render tree needs, then redo slices of it whenever a class changes or a node is inserted. So the order in which a selector is tested against the tree is a genuine performance question, not a stylistic one.
Take a concrete rule: .nav li a. Read left to right, it says "an a, inside an li, inside something with class nav." Read as an instruction for a matcher, left to right would mean: find every .nav, then walk down into every descendant looking for an li, then walk further down looking for an a — a speculative descent into a subtree that might be huge, for a selector that in the end only cares about a elements.
Right to left: the key selector
Browsers do it backwards. They take the rightmost simple selector — here, a — called the key selector, and match that first. The engine already has to visit every element to build the render tree, so as it visits each a element it asks: does this rule's rightmost part match me? Only for elements where the answer is yes does it bother walking up the ancestor chain, checking "is my parent (or an ancestor) an li?" and then "does an ancestor of that have class nav?" If an a isn't inside an li at all, the browser has already discarded the rule for that element after one failed ancestor check, instead of having speculatively descended into every .nav subtree looking for one.
This is why an overly generic key selector is a real performance smell. A rule like * { box-sizing: border-box } or div span has a cheap-looking rightmost part that matches enormous numbers of elements, so the "does this match me" check has to run — and often fail on the ancestor walk — very often. A rule like .icon-search as the key selector immediately filters to a handful of elements before any ancestor walk happens at all.
Specificity: the (a, b, c) triple
Matching finds every rule that applies to an element. It's completely normal for several of them to set the same property — a reset sets color, a component class sets color, a utility class sets color — so the browser needs a rule for picking a winner. The first tool it reaches for is specificity, a three-part score computed straight from a selector's own text, with no knowledge of the DOM at all:
- a — the number of ID selectors (
#hero) - b — the number of class selectors, attribute selectors, and pseudo-classes (
.intro,[href],:hover) - c — the number of element selectors and pseudo-elements (
div,::before)
Two selectors are compared left to right, one column at a time: whichever has more IDs wins outright, regardless of how many classes or elements the other has; only if a ties do you compare b; only if b also ties do you compare c. #hero is (1, 0, 0) and beats .nav li a at (0, 1, 2) even though the second selector is much longer — specificity is not selector length.
/* (0, 0, 1) — one element selector */
p { color: #444; }
/* (0, 1, 0) — one class selector, beats the rule above */
.intro { color: teal; }
/* (0, 1, 0) — same specificity as .intro, a tie */
.lead { color: coral; }If both .intro and .lead match the same <p class="intro lead">, their specificity ties at (0, 1, 0). Specificity alone can't break that tie — it falls to source order, covered below.
Two declarations sit outside this whole scoring system. An inline style attribute on the element beats any selector-based rule, no matter its specificity, because it isn't scored on the (a, b, c) scale at all — it's a separate, higher tier. And a declaration marked !important is promoted above the entire normal cascade, again independent of specificity, which is exactly why it can override even an inline style.
.lead { color: coral !important; } /* now wins even against an inline style */<p class="intro lead" style="color: black;">…</p>
<!-- without !important: inline black wins -->
<!-- with !important on .lead: coral wins -->Try it directly: toggle rules on and off against the same element and watch which one paints it.
<p class="intro" id="lead">Sample text</p>
Sample text
#lead has an id — one id beats any number of classes or types.
The cascade doesn't total up points across every matching rule — it picks one winner in three passes. First origin and importance: an !important author declaration beats every normal declaration, no matter how specific the normal one is. Then specificity, compared as a tuple — inline style, then id count, then class count, then type count, in that order — where a single id always outranks any number of classes, and a pile of classes always outranks any number of type selectors. Only when the tuple ties exactly does source order decide: whichever matching rule appears later in the stylesheet wins.
The cascade: the full decision order
Specificity is only one stage. The complete algorithm the browser runs, for a given property on a given element, to pick one winning declaration among every matched rule, is the cascade:
1. Origin and importance. Rules come from different places — the browser's own default (user-agent) stylesheet, the user's own stylesheet or settings, and the page's author stylesheet — and each can be normal or !important. Roughly in increasing priority: user-agent normal, then user normal, then author normal, then author !important, then user !important (the browser's own !important sits at the very top, above all of these, for accessibility overrides it must always win). Only if two declarations land in the same tier does the cascade move on.
2. Specificity. Within the same origin-and-importance tier, the (a, b, c) triple decides, exactly as above.
3. Source order. If specificity also ties, the declaration that appears later in the CSS — later in the same file, or in a file linked later — wins. This is why .lead beat .intro above: same specificity, later in the stylesheet.
.intro { color: teal; } /* loses: earlier, same specificity as .lead */
.lead { color: coral; } /* wins: later, tie broken by source order */Inheritance and the four keywords
Not every property is decided by matching a rule at all. Some CSS properties — mostly the ones about typography, like color, font-family, font-size, line-height — inherit: if nothing matched the element for that property, it takes its parent's computed value instead of the property's built-in default. Layout-affecting properties like width, margin, and display do not inherit, precisely because inheriting a parent's box geometry into every child would make layout unusable.
Four keywords let you control this explicitly on any property:
inherit— force this property to take the parent's computed value, even if it doesn't normally inherit.initial— reset to the property's specification-defined default, ignoring inheritance entirely.unset—inheritif the property is naturally inheriting,initialotherwise; effectively "act as if no rule and no inheritance chain touched this."revert— roll back to the value the property would have gotten from an earlier origin in the cascade (typically the user-agent stylesheet), useful for undoing an author rule without guessing its built-in default.
:is() and :where()
Two modern selector functions change how selectors are grouped without changing what they mean for matching, but they diverge sharply on specificity. :is(.a, .b, .c) matches anything .a, .b, or .c would, and its specificity is that of its most specific argument — so :is(#id, .class) counts as (1, 0, 0). :where() matches identically but always contributes zero specificity, no matter what's inside it.
/* specificity (1, 0, 0) — counts as an ID selector */
:is(#hero, .card) h2 { color: teal; }
/* specificity (0, 0, 1) — the #hero inside contributes nothing */
:where(#hero, .card) h2 { color: coral; }:where() is the tool of choice for library and reset authors: it lets you write a selector as specific-looking as you like for readability, while guaranteeing it never outranks whatever the page author writes afterward.
Where this leaves a property
At the end of all of this — matching finds the candidates, the cascade picks a winner — a property has exactly one cascaded value (or none, if nothing matched and it doesn't inherit). That's still not the number layout will use: relative units haven't been resolved, percentages haven't been computed against anything, and var() references haven't been substituted. Computed style picks up exactly there and follows a property through the rest of the pipeline — specified, computed, used, actual — to the value that finally reaches a box on screen.
Go deeper
- MDN — Specificity — The canonical (a, b, c) definition, worked examples, and how :is()/:where()/:not() affect the count.
- MDN — Cascade — The full origin-and-importance ordering, laid out tier by tier, that this lesson's cascade section summarizes.
- W3C Selectors Level 4 spec — The formal selector-matching model, including the right-to-left evaluation this lesson describes informally.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- Why does a browser match a selector like .nav li a by starting from its rightmost simple selector instead of its leftmost?
- What makes a key selector 'bad' for matching performance, even if the full selector is very specific?
- Write the (a, b, c) specificity triple for #hero .card p, and explain which column dominates the comparison.
- Two rules have identical specificity. What decides which one wins, and does it matter which file each rule is in?
- Where do an inline style attribute and !important sit relative to the normal specificity comparison?
- Why does :where(#hero) have zero specificity even though #hero alone would be (1, 0, 0)?
- What is the difference between inherit, initial, unset, and revert?