gap·map

CSS specificity & cascade

[ junior depth ]

The cascade picks one winner per property

When several rules set the same property on the same element, the cascade is the algorithm that picks the winner. At this level you need two of its steps cold: specificity, then source order.

How specificity is calculated

Count a selector's weight as three separate columns, ID-CLASS-TYPE:

  • #id → ID column
  • .class, [attr], :hover → CLASS column
  • p, ::before → TYPE column
  • * and combinators (>, +, ~, space) → count nothing

Compare column by column, left to right. The load-bearing rule: columns never overflow into each other.

#nav a { color: green; }                  /* 1-0-1, wins */
.site .nav .list .item a { color: red; }  /* 0-4-1, loses */

A common wrong belief: "eleven classes beat one ID". They don't. 0-11-0 still loses to 1-0-0, because specificity is not a base-10 number.

What happens when specificity is equal

Equal specificity means the later declaration wins:

body h1 { color: green; }
html h1 { color: purple; } /* wins: both 0-0-2, this one is later */

Note what does not matter: how "close" the selector's elements are to the target in the DOM. body h1 is no closer than html h1 in any way the cascade cares about.

Inline styles, !important, and inheritance

Three placements sit outside the columns. A style="" attribute beats any selector. !important beats normal declarations, including inline ones. And inheritance is not the cascade: a rule that targets the element directly, however weak, beats any value inherited from a parent, even a #id parent.

When a style doesn't apply, read it in DevTools: crossed-out declarations lost the cascade, and the panel sorts winners from the top down. Specificity is only the third check the browser runs on competing declarations. Origin and cascade layers come first, and the middle-level notes cover both.

[ middle depth ]

Where specificity sits in the cascade

The full sorting order the browser applies to competing declarations:

  1. Origin and importance: user-agent < user < author (for normal declarations)
  2. Cascade layers
  3. Specificity
  4. Source order

Most "CSS doesn't work" confusion is someone debugging step 3 when the fight was already decided at step 1 or 2.

How @layer changes what wins

@layer base, theme;          /* order fixed HERE, at first declaration */
@layer theme { .btn { background: purple; } }  /* later layer, wins in the layer world */
@layer base  { .btn.primary { background: green; } } /* more specific, irrelevant */
.btn { background: gray; }   /* unlayered, beats BOTH */

Two rules to internalize here. Among layered rules, the later layer wins; specificity only breaks ties within one layer. And unlayered author styles beat all layered ones (for normal declarations). People assume that putting styles in a layer makes them stronger. It is exactly backwards: layers are for organized weakness.

Re-opening @layer theme {} later doesn't move it. A layer's position is fixed by its first declaration.

:is() vs :where() specificity

Both take selector lists and both match identically. They differ only in specificity: :is() adopts its most specific argument, while :where() is always zero:

:is(#main, article) p { color: red; }     /* 1-0-1 */
:where(#main, article) p { color: blue; } /* 0-0-1, any later `p` rule can override */

:where() exists for resets and library defaults: styles that apply until anyone writes anything. Both are also forgiving: one invalid selector in the list doesn't kill the rule, unlike a plain selector list.

How !important competes

Important declarations compete only against other important ones, and among them specificity and source order still apply as usual. !important is a second bracket, not a trump card. Two legal specificity-management tricks belong in the same drawer: [id="x"] gives class-level weight to an id, and .a.a doubles a class to nudge weight up without inventing new names. When !important meets layers and other origins, the order inverts; the senior notes cover that inversion.

[ senior depth ]

The complete precedence stack

For one property on one element, bottom to top:

UA normal → user normal → author normal (layers in declaration order, then unlayered) → keyframe animations → author !important → user !important → UA !important → transitions.

Interviewers at this band probe two points in that stack: a running transition beats everything, and !important inverts the order. The inversion runs across origins (user-agent important is the strongest, which is how user accessibility overrides are guaranteed to work) and across layers: for important declarations, the first-declared layer wins, and important layered beats important unlayered.

@layer base, override;
@layer override { .btn { color: red !important; } }  /* loses */
@layer base    { .btn { color: green !important; } } /* wins: earlier layer */

The inversion is the design rationale of the whole feature: a framework's early layer can expose !important escape hatches that user code can't accidentally out-shout.

Where inline styles and animations fit

Inline style="" occupies its own slot: normal inline beats all author normal declarations; important inline beats all author important. Animations sit between normal and important. @keyframes values override normal declarations while running, but declarations with !important beat the animation (and !important inside keyframes is ignored entirely). CSS nesting computes specificity like :is(): the outer selector list contributes its most specific option.

Cascade architecture

At this band the job is not computing weights, it is designing the sheet so nobody has to:

@layer reset, tokens, third-party, components, utilities;
@import url("vendor.css") layer(third-party);  /* quarantine */
  • Layer order becomes the single source of truth for who wins; specificity fights within a layer stay small and local.
  • Library and reset styles ship at zero specificity via :where(), as defaults that any consumer selector overrides.
  • Third-party CSS gets imported into an early layer, so even its ID-heavy selectors lose to your unlayered app code and nobody has to reach for !important.
  • Utility classes go in the last layer, which is the honest version of what utility frameworks used to do with !important.

None of this outranks an inline style or a running transition. Those two sit above every author layer no matter how you order them.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

unlocks

more CSS