Skip to content
academia.sh

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.

  1. Source and importance. Where the declaration comes from, and whether it carries !important.
  2. Being inline. Declarations coming from the element’s style attribute go first.
  3. Specificity. How narrow a set the selector targets.
  4. 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: (a,b,c)(a, b, c).

  • aa: the number of id selectors in the selector.
  • bb: the total number of class selectors, attribute selectors, and pseudo-classes.
  • cc: 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 1,0,11,0,1 — because the list contains one id selector.

:where(), on the other hand, contributes zero. :where(.measurement-table) td’s triple is 0,0,10,0,1, 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 c=2c = 2.

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 aa, if tied bb, if tied cc.

// 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 .value instead of .measurement-table tbody tr td.value reaches 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 (a,b,c)(a, b, c) 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.
  • !important moves 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.

Start typing to search.

↑↓ Esc navigate · open · close