Lesson 07 / 26
Cascade and Specificity
Ordering declarations that write to the same property by source, importance, specificity, and order criteria; computing the specificity triple and why it cannot be read in base ten.
Contents
By the end of the previous lessons, more than one rule matches the same element. A cell
matches the td selector, the .measurement-table td selector, the td.missing
selector, and the #content td selector, all at once. If all four write to the
text-align property, what is the cell’s alignment?
The answer to this question is not intuition, it is a defined ordering. The name of that ordering is cascade, and it is where the first letter in the language’s name comes from. This lesson applies the cascade’s steps in order and makes each step computable.
The Cascade Orders Declarations
An important distinction to start with: the cascade orders not rules, but declarations. The ordering is done for a single element and a single property. Two declarations of the same rule compete separately; one winning does not mean the other wins too.
After the declarations valid for an element-property pair are collected, four criteria are applied in order. If a winner emerges at one criterion, the next criterion is not consulted.
- Source and importance. Where the declaration comes from, and whether it carries
!important. - Being inline. Declarations coming from the element’s
styleattribute go first. - Specificity. How narrow a set the selector targets.
- Order. Among declarations of the same specificity, the one written later wins.
There are three sources: the browser’s own default style, styles the user has defined, and styles the document’s author has written. Their normal ordering, weakest to strongest, is: browser, user, author.
Specificity Is a Triple
Specificity is not a single number, it is an ordered triple of three numbers: .
- : the number of id selectors in the selector.
- : the total number of class selectors, attribute selectors, and pseudo-classes.
- : the total number of type selectors and pseudo-elements.
The universal selector * and combinators contribute to no column.
The following program converts a selector string into this triple.
// specificity.mjs — converts a selector string into the (a, b, c) triple function specificity(selector) { let s = selector; let a = 0, b = 0, c = 0; // :where(...) contributes nothing to specificity -> strip first s = s.replace(/:where\([^()]*\)/g, ""); // :is( ) / :not( ) / :has( ) -> inherit the highest specificity inside const wrapping = /:(?:is|not|has)\(([^()]*)\)/g; let m; while ((m = wrapping.exec(s)) !== null) { const highest = m[1] .split(",") .map((p) => specificity(p.trim())) .reduce((top, o) => (compare(o, top) > 0 ? o : top), [0, 0, 0]); a += highest[0]; b += highest[1]; c += highest[2]; } s = s.replace(wrapping, ""); // pseudo-elements (::x) -> c; pseudo-classes (:x) -> b const pseudoElement = s.match(/::[a-z-]+/g) ?? []; c += pseudoElement.length; s = s.replace(/::[a-z-]+/g, ""); a += (s.match(/#[\w-]+/g) ?? []).length; s = s.replace(/#[\w-]+/g, ""); b += (s.match(/\.[\w-]+/g) ?? []).length; // class s = s.replace(/\.[\w-]+/g, ""); b += (s.match(/\[[^\]]*\]/g) ?? []).length; // attribute s = s.replace(/\[[^\]]*\]/g, ""); b += (s.match(/:[a-z-]+(\([^()]*\))?/g) ?? []).length; // pseudo-class s = s.replace(/:[a-z-]+(\([^()]*\))?/g, ""); c += (s.match(/[a-z][\w-]*/gi) ?? []).length; // type selector (* not counted) return [a, b, c]; } const compare = (x, y) => x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; const selectors = [ "*", "td", ".measurement-table td", "td.missing", "tbody > tr > td", ".measurement-table tbody td:not(.name)", "input[type=\"number\"]", "#content td", ".measurement-table td.value.missing", "td.missing::after", ":where(.measurement-table) td", ":is(#content, .side) p", "tr:has(td.missing)", ]; for (const s of selectors) { const [a, b, c] = specificity(s); console.log(`${a},${b},${c} ${s}`); }
0,0,0 * 0,0,1 td 0,1,1 .measurement-table td 0,1,1 td.missing 0,0,3 tbody > tr > td 0,2,2 .measurement-table tbody td:not(.name) 0,1,1 input[type="number"] 1,0,1 #content td 0,3,1 .measurement-table td.value.missing 0,1,2 td.missing::after 0,0,1 :where(.measurement-table) td 1,0,1 :is(#content, .side) p 0,1,2 tr:has(td.missing)
Five lines call for a further reading.
.measurement-table td and td.missing gave the same triple: each has one class and
one type. A selector’s length or how “specific it looks” does not enter the calculation;
only the counts do.
:not(), :is(), and :has() are not counted themselves; they inherit the highest
specificity among the members of their list. :is(#content, .side) p therefore gave
— because the list contains one id selector.
:where(), on the other hand, contributes zero. :where(.measurement-table) td’s triple
is , exactly as if only td had been written. This is a way to narrow a selector
without raising specificity.
A pseudo-element counts toward the third column: td.missing::after’s triple has .
The Triple Is Not a Base-Ten Number
A common mistake is reading the triple as a hundreds-tens-ones digit. This reading breaks down at values of ten and above, because there is no carrying between columns. The comparison is lexicographic order: first , if tied , if tied .
// not-decimal.mjs — the specificity triple is not a base-ten number const compare = (x, y) => x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; const pairs = [ [[0, 11, 0], [1, 0, 0]], [[0, 0, 12], [0, 1, 0]], [[1, 0, 0], [0, 99, 99]], ]; for (const [x, y] of pairs) { const s = compare(x, y); const winner = s > 0 ? x : s < 0 ? y : "tie"; console.log(`${x.join(",")} vs ${y.join(",")} -> winner ${Array.isArray(winner) ? winner.join(",") : winner}`); console.log(` read as base ten: ${x[0]*100+x[1]*10+x[2]} vs ${y[0]*100+y[1]*10+y[2]}`); }
0,11,0 vs 1,0,0 -> winner 1,0,0 read as base ten: 110 vs 100 0,0,12 vs 0,1,0 -> winner 0,1,0 read as base ten: 12 vs 10 1,0,0 vs 0,99,99 -> winner 1,0,0 read as base ten: 100 vs 1089
The first line is critical: a selector carrying eleven classes cannot beat a single id selector. Reading it as base ten would say the opposite. The result is that the only way to override a declaration written with an id selector is to write an id selector as well. This is the reason for the “style is written with a class” rule from the selectors lesson.
How the Cascade Is Applied
The following program puts seven declarations into full order. Source layers are numbered weakest to strongest; the ordering criteria are applied in sequence.
// cascade.mjs — orders declarations that write to the same property and finds the winner // (specificity calculation is from the previous program; given by hand here) // layer order: smaller number = weaker const LAYER = { "browser-normal": 0, "user-normal": 1, "author-normal": 2, "author-inline": 3, "author-important": 4, "user-important": 5, "browser-important": 6, }; const declarations = [ { source: "browser-normal", selector: "td", specificity: [0,0,1], order: 1, value: "left" }, { source: "author-normal", selector: "td", specificity: [0,0,1], order: 2, value: "left" }, { source: "author-normal", selector: ".measurement-table td", specificity: [0,1,1], order: 3, value: "right" }, { source: "author-normal", selector: "td.name", specificity: [0,1,1], order: 4, value: "start" }, { source: "author-normal", selector: "#content td", specificity: [1,0,1], order: 5, value: "center" }, { source: "author-important",selector: ".measurement-table td.missing", specificity: [0,2,1], order: 6, value: "end" }, { source: "user-important", selector: "td", specificity: [0,0,1], order: 7, value: "justify" }, ]; const compare = (x, y) => LAYER[x.source] - LAYER[y.source] || x.specificity[0] - y.specificity[0] || x.specificity[1] - y.specificity[1] || x.specificity[2] - y.specificity[2] || x.order - y.order; function resolve(candidates) { const sorted = [...candidates].sort(compare); console.log("weakest to strongest:"); for (const d of sorted) { const s = d.specificity.join(","); console.log(` ${d.source.padEnd(17)} ${s} order=${d.order} ${d.selector.padEnd(28)} -> ${d.value}`); } console.log(`winner: text-align: ${sorted.at(-1).value}`); console.log(""); } console.log("=== all declarations ==="); resolve(declarations); console.log("=== author-normal only ==="); resolve(declarations.filter((d) => d.source === "author-normal")); console.log("=== specificity tied, order decides ==="); resolve(declarations.filter((d) => d.source === "author-normal" && d.specificity.join() === "0,1,1"));
=== all declarations === weakest to strongest: browser-normal 0,0,1 order=1 td -> left author-normal 0,0,1 order=2 td -> left author-normal 0,1,1 order=3 .measurement-table td -> right author-normal 0,1,1 order=4 td.name -> start author-normal 1,0,1 order=5 #content td -> center author-important 0,2,1 order=6 .measurement-table td.missing -> end user-important 0,0,1 order=7 td -> justify winner: text-align: justify === author-normal only === weakest to strongest: author-normal 0,0,1 order=2 td -> left author-normal 0,1,1 order=3 .measurement-table td -> right author-normal 0,1,1 order=4 td.name -> start author-normal 1,0,1 order=5 #content td -> center winner: text-align: center === specificity tied, order decides === weakest to strongest: author-normal 0,1,1 order=3 .measurement-table td -> right author-normal 0,1,1 order=4 td.name -> start winner: text-align: start
The second run shows the criteria being applied in order: staying within a single source, the winner was determined by specificity. In the third run, specificity was also tied, and the decision came down only to write order; the one written later won.
The Importance Flag Reverses the Ordering
When !important is written at the end of a declaration, that declaration moves to a
separate layer. The order in the layer list must be read carefully: for important
declarations, source ordering reverses. In normal declarations, author beats user; in
important declarations, user beats author.
This reversal is not an accident, it is a design decision. A style a user writes to meet
their own needs — enlarging text, increasing contrast, stopping motion — should be able to
override the page author’s decisions. The output’s first block, with the
user-important layer sitting at the very top, shows exactly this.
On the author’s side, the cost of writing !important is opening an exception layer in
one’s own style file. Once written, the only way to override it is to write
!important again, and at that point the specificity calculation starts over. The
actionable rule is: !important is written not because a conflict cannot be resolved, but
because resolving it was not attempted; lowering specificity is tried first.
There is another mechanism that sits between source and importance: cascade layers
defined with @layer. Author style opens named layers, and layer order is evaluated
before specificity; this way, an entire base layer, whatever its specificity, can be
overridden by a layer above it.
Keeping Specificity Low
This lesson’s practical result is a writing discipline.
- Selectors are kept as flat as possible. Writing
.measurement-table .valueinstead of.measurement-table tbody tr td.valuereaches the same element with a lower triple. - An id selector is not used in style; a rule that opens the first column once forces every later fix to write an id as well.
- If a selector needs to be narrowed by context,
:where()is written; the narrowing happens, specificity does not rise.
/* station.css — step 6: notation that keeps specificity flat */ :where(.measurement-table) .value { text-align: right; } /* 0,1,0 */ :where(.measurement-table) .name { text-align: start; } /* 0,1,0 */ .missing { color: #8a1c1c; } /* 0,1,0 */
All three rules carry the same triple. This means that conflicts between them are resolved only by write order — the state that is easiest to read and predict.
Summary
- The cascade orders not rules but declarations; the ordering is done for a single element and a single property.
- The criteria are applied in order: source and importance, being inline, specificity, write order. If an earlier criterion produces a winner, the next one is not consulted.
- Specificity is the triple: id count, class–attribute–pseudo-class count, type–pseudo-element count. The comparison is lexicographic; there is no carrying between columns.
:is(),:not(), and:has()inherit the highest specificity inside them;:where()contributes zero and allows narrowing without raising specificity.!importantmoves a declaration to a separate layer and reverses source ordering; this is defined so that user style can override author decisions.
Next Step
The cascade picks the winner among declarations that match an element. What if no
declaration matches at all? If no rule writes color to a paragraph, what is that
paragraph’s color? The next lesson shows that this gap is filled by two separate
mechanisms — inheritance and initial value — and defines the keywords that call each of
them explicitly.
To keep your progress and take notes, Log in
My notes
Log in to take notes.