Course outline · 0% complete

0/30 lessons0%

Course overview →

The cascade and specificity

lesson 4-3 · ~10 min · 12/30

When rules collide

The single most common CSS frustration, where a rule sits right there and the browser appears to ignore it, is never random. A fixed algorithm, the cascade, decides every conflict, and knowing it turns guessing into prediction.

In the lesson 4-1 example, two rules targeted the same paragraph:

p { color: gray; }
.warning { color: red; }
<p class="warning">Careful!</p>

The text came out red. The cascade decides every such conflict, checking in order.

StepTestNote
1importance!important beats everything
2specificitymore specific selectors win
3source orderon a tie, the last rule wins

!important should be used almost never, because it makes later CSS unfixable. Here .warning is more specific than p, so red wins at step 2 and source order never comes up.

Separately, some properties such as color and font-family inherit from parent to child when nothing targets the child directly. That is why setting fonts on body in lesson 4-2 styled the whole page.

The specificity score

Count a selector's parts into three buckets, then compare bucket by bucket.

BucketCountsExample
IDseach #id#site-title gives (1,0,0)
classeseach .class.card .warning gives (0,2,0)
typeseach tag namenav a gives (0,0,2)

Compare left to right like version numbers, so (1,0,0) beats (0,9,9) because one ID outranks any number of classes. Inline styles, meaning a style="..." attribute in the HTML, sit above all selectors.

Practical advice that saves hours: style with classes almost everywhere. Flat class selectors keep every score at (0,1,0), so the winner is simply whichever rule comes last in the file.

The cascade then becomes boring, and boring is exactly what a stylesheet should be.

p.warning#site-titlestyle="..."type (0,0,1)class (0,1,0)id (1,0,0)inline: beats all selectors
Each step up the ladder outranks everything below it, no matter how many lower-rank parts a selector stacks up.

An ID against a class

For <p id="intro" class="lead"> with both #intro { color: green; } and .lead { color: purple; }, and .lead written last, the text is green.

IDs outrank classes, and source order only breaks ties.

SelectorScorePosition
#intro(1,0,0)first
.lead(0,1,0)last

The ID rule wins regardless of position in the file, because the comparison reaches a decision in the first bucket. This is the situation that makes people add !important, when moving the color onto a class would fix it cleanly.

A specificity calculator

A small function splitting a selector on spaces and counting IDs, classes, and types into the three-bucket score.

function specificity(selector) {
  const parts = selector.split(" ");
  let ids = 0, classes = 0, types = 0;
  for (const part of parts) {
    if (part.startsWith("#")) ids++;
    else if (part.startsWith(".")) classes++;
    else types++;
  }
  return [ids, classes, types];
}

console.log(specificity("#intro"));
console.log(specificity(".card .warning"));
console.log(specificity("nav a"));

Output

[ 1, 0, 0 ]
[ 0, 2, 0 ]
[ 0, 0, 2 ]
SelectorScore
#intro(1,0,0)
.card .warning(0,2,0)
nav a(0,0,2)

The else branch catches bare tag names, which is why every part lands in exactly one bucket. Real browsers handle more cases than this, including pseudo-classes in the class bucket, and the three-bucket shape is identical.

Deciding which selector wins

wins(a, b) returns whichever selector has higher specificity, comparing IDs first, then classes, then types, and returning b on a complete tie because the later rule wins by source order.

function specificity(selector) {
  const parts = selector.split(" ");
  let ids = 0, classes = 0, types = 0;
  for (const part of parts) {
    if (part.startsWith("#")) ids++;
    else if (part.startsWith(".")) classes++;
    else types++;
  }
  return [ids, classes, types];
}

function wins(a, b) {
  const sa = specificity(a);
  const sb = specificity(b);
  for (let i = 0; i < 3; i++) {
    if (sa[i] > sb[i]) return a;
    if (sa[i] < sb[i]) return b;
  }
  return b;
}

console.log(wins("#intro", ".card .warning"));
console.log(wins("nav a", ".lead"));
console.log(wins("p", "h1"));

Output

#intro
.lead
h1
ComparisonDecided in bucketWinner
#intro against .card .warningIDs#intro
nav a against .leadclasses.lead
p against h1none, a tieh1, the later rule

Both scores are computed up front, then the loop stops at the first bucket where they differ. A loop that finishes without deciding means the scores are identical, which is precisely the case source order exists to settle.