---
title: 'Cascade and Specificity'
source: 'https://academia.sh/en/courses/css-fundamentals/cascade-and-specificity'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:16+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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$: the number of id selectors in the selector.
- $b$: the total number of class selectors, attribute selectors, and pseudo-classes.
- $c$: 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.

```js
// 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,1$ — because the list contains one id selector.

`:where()`, on the other hand, contributes zero. `:where(.measurement-table) td`'s triple
is $0,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 = 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 $a$, if tied $b$, if tied $c$.

```js
// 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.

```js
// 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.

```css
/* 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)$ 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.
