CSSGENERALSPEC-EVOLVINGBROWSER-SPECIFIC

The Cascade

Origin and importance, then context, then element-attached styles, then layers, then specificity, then order — a six-step sort that decides every property on every element, in that order.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

When two declarations both apply to an element, which one wins, and at which step of the sort did the other one lose?

The user intent

Someone wants the button to be the brand colour. They wrote a rule that says so, and the button is grey. Nothing in the interface tells them why.

The obvious build

The more specific selector wins, and if that fails, !important wins. If neither works, add another class and try again.

Why it breaks

Specificity is the fifth of six comparisons, not the first. A declaration can lose to something with a laughably weak selector because it lost earlier — to a cascade layer, to an inline style, or to a user-agent !important rule.

How it breaks in a real browser
  • Specificity is the fifth of six comparisons, not the first. A declaration can lose to something with a laughably weak selector because it lost earlier — to a cascade layer, to an inline style, or to a user-agent !important rule.
  • It cannot explain a rule losing to an *identical* selector defined later in a file, which is the most common real cause and has nothing to do with specificity at all.
  • !important does not simply "win". It moves the declaration into a different importance band, and it reverses the ordering of cascade layers — so an !important declaration in the layer you thought was strongest is now the weakest of the important ones.
  • It has no account of transitions and animations, which sit at specific positions in the sort and are the reason a value can visibly refuse to change while devtools shows your rule as the winner.
  • The escalation it recommends is one-directional. Every !important added to end an argument makes the next argument harder, and a codebase converges on a state where the only tool left is another !important plus an inline style.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Step 1 — origin and importance, together. Highest to lowest: transition declarations; important user-agent; important user; important author; animation declarations; normal author; normal user; normal user-agent. Read that list twice — importance *inverts* the origin order, which is the whole point. It is how a user's own high-contrast stylesheet can override an author who tried to force a colour.
  • Step 2 — context. Declarations from different shadow trees are compared next. For normal declarations the outer tree wins, so a page can restyle a component; for important declarations the inner tree wins, so a component can protect an invariant (Shadow DOM and the Composed Tree).
  • Step 3 — element-attached styles. A style attribute beats any rule in the same origin and importance band. It is not "infinite specificity" — it is an earlier and separate comparison, which is why an !important rule in a stylesheet still beats a normal inline style.
  • Step 4 — cascade layers. Within one origin, @layer establishes an explicit precedence order that you declare. Unlayered normal declarations beat every layered normal declaration. For important declarations the layer order reverses, and unlayered important loses to layered important.
  • Step 5 — specificity. Only now does the selector's shape matter, compared as a three-part tuple (Specificity).
  • Step 6 — order of appearance. Everything still tied is resolved by document order: the last declaration wins. This is the step most bugs actually lose at, and it is the one nobody looks for.
  • The output is one cascaded value per property per element. That value is then defaulted, inherited if absent, and computed — a separate stage with its own rules (Inheritance and Computed Style).

What this makes the browser do

And which of it is avoidable.

  • Collecting matched declarations per element from the rule index, then sorting them by the six criteria above. Engines do not literally sort a list per element; they exploit the fact that the criteria are mostly precomputable per rule.
  • Maintaining a matched-properties cache so that elements with identical matched rules and identical inherited context can share a computed style rather than each running the sort (Style Calculation).
  • Re-running the sort for every element in the invalidation set whenever the DOM, a class, an attribute or a stylesheet changes (Style Invalidation).
  • The avoidable half: declarations that never win. A codebase with five competing rules per property still computes all five and discards four, on every recalculation, for every matching element.

The sort, in order

This table is the lesson. When a declaration loses, it lost at exactly one of these rows, and knowing which one tells you what to change. Working down it in order is faster than any amount of staring at selectors.

Note the shape of row one. Importance does not add weight to a declaration; it moves it into a different band, and the bands for important declarations are ordered *opposite* to the bands for normal ones. That inversion is not a quirk — it is the mechanism that guarantees a user can always override an author, which is an accessibility guarantee written into the cascade itself.

  • Steps run strictly in order. A win at step 1 is not revisited at step 5, no matter how many ids the loser had.
  • The result is a single cascaded value per property. Absence of a cascaded value is what triggers inheritance or the initial value (Inheritance and Computed Style).
  • Devtools shows the outcome, not the reasoning. If you cannot see why something lost, check for a layer, an inline style, or an animation before you count selectors.
#ComparisonWhat winsThe failure it explains
1Origin and importanceTransitions, then important UA, important user, important author, then animations, then normal author, normal user, normal UAYour rule losing to a user stylesheet or to forced-colors mode; a value refusing to change during a transition
2Context (shadow trees)Outer tree for normal declarations; inner tree for important onesA page style failing to reach into a web component, or a component's important rule refusing to be overridden
3Element-attached stylesThe style attribute beats stylesheet rules in the same bandA framework or animation library writing inline styles that your stylesheet cannot beat
4Cascade layersUnlayered beats layered for normal; the order reverses for importantA utility class losing to a component rule, or an !important in the "strongest" layer suddenly being the weakest
5SpecificityThe higher (a, b, c) tuple (Specificity)The classic override fight, and the only step with no natural ceiling
6Order of appearanceThe last declaration in document orderTwo identical selectors in two files, where the bundler decided the outcome

Layers make precedence something you declare

Before @layer, precedence inside the author origin was an emergent property of selector shapes and file order — two things that are decided by different people at different times. Layers let you state the ordering once, at the top, and then write the simplest selector that expresses intent inside each layer.

The rule to internalise is that layer order beats specificity. A single-class rule in a later layer beats a three-id rule in an earlier one. That inversion is the point: it means "this is a utility, it wins" becomes a structural statement rather than an escalation.

The second rule to internalise is that unlayered normal declarations sit above every layer. That is usually what you want during a migration — existing CSS keeps working while you move vendor styles into a low layer — and it is also the thing that silently defeats a layer architecture when one global file never gets migrated.

Ending the same argument two ways
Escalate
/* their rule */
#datepicker .dp-cell.dp-selected { background: #6b7280; }

/* ours, third attempt */
#app #datepicker .dp-cell.dp-selected.is-active {
  background: var(--brand) !important;
}
Order
@layer vendor, components;

@layer vendor { /* @import their stylesheet here */ }

@layer components {
  .cal-day--selected { background: var(--brand); }
}

The first wins today and raises the floor for every future override, including your own — the next person needs four ids and an !important to change this colour. The second states the precedence relationship once, so every subsequent rule in components beats every rule in vendor with no escalation at all. Neither is "cleaner"; the second has a lower cost for the next change.

Declaring the order once
1/* First thing in the entry stylesheet. Declaring the order up front means
2 it does not depend on which file the bundler happens to emit first. */
3@layer vendor, base, components, utilities;
4
5@layer vendor {
6 /* Third-party CSS lands here, ids and all. */
7 #datepicker .dp-cell.dp-selected { background: #6b7280; }
8}
9
10@layer components {
11 /* One class. It wins anyway, because vendor is an earlier layer. */
12 .cal-day--selected { background: var(--brand); }
13}
14
15@layer utilities {
16 /* Utilities win over components by layer order, not by specificity,
17 so they stay one class deep and stay readable. */
18 .bg-surface { background: var(--surface); }
19}
20
21/* Not in any layer: beats ALL of the above for normal declarations.
22 This is the line that quietly defeats the architecture above. */
23.legacy-banner { background: #fde68a; }

Read the last rule as the warning it is. Unlayered normal CSS is not "neutral" — it is the top of the normal author band, above every layer you declared.

Reading a cascade failure

Almost every "CSS is broken" report is one of a small number of shapes. The value of the six-step model is that it turns an open-ended search into a checklist you can run in under a minute in the Styles pane.

Work the steps in order, top to bottom, and stop at the first one that explains what you see. Counting selector weights first is the most common mistake, because it is the step people know — and it is fifth.

Which step did it lose at?
TriggerSymptomCauseResponse
Rule is struck through, and the winner has a much weaker selectorA one-class rule beats your three-class ruleLayer order, or the winner is unlayered while yours is in a layerCheck the layer annotation in the Styles pane before counting anything (A Mental Model of the Devtools).
Winner shows as element.styleNo stylesheet rule can beat itStep 3 — an inline style, usually written by a framework, an animation library or a drag handlerFix it where it is written. If you truly cannot, an !important stylesheet rule beats a normal inline style; note why in a comment.
Value visibly refuses to change while an animation runsDevtools shows your rule as the winner, the screen disagreesStep 1 — animation and transition declarations sit above normal author declarationsChange the animation, or the state that drives it. Overriding it from a rule cannot work by construction.
Your colours are ignored entirely in one user's screenshotHigh-contrast palette, your brand colours goneStep 1 — forced-colors injects user-agent declarations you are not meant to beatDesign for it: use forced-colors media queries and system colour keywords rather than fighting it (Contrast, Colour and Motion).
Identical selectors, different files, wrong one winsWorks locally, breaks after a build or on one route onlyStep 6 — source order, decided by the bundler or by which chunk loaded firstMake it explicit with a layer. Relying on emitted order is relying on a build detail (Code Splitting).
Two !important declarations both applyThe escalation stopped workingAmong important declarations, layer order is reversed and specificity still decides tiesRemove both and express the relationship with layers instead. The argument does not have a top.

How to build it

Most important first.

  • Debug by naming the losing step. Open the element, look at the crossed-out declarations, and ask which of the six comparisons ended it. Devtools tells you — the answer is in the panel, not in a guess (A Mental Model of the Devtools).
  • Use @layer to make precedence explicit and architectural instead of emergent. @layer reset, base, components, utilities; at the top of your entry stylesheet is one line that removes a category of argument, because a utility can now beat a component with a weaker selector.
  • Keep the third-party stylesheet in the lowest layer. @layer vendor, app; with the vendor CSS imported into vendor means your normal rules beat theirs regardless of how many ids they used, and you never write !important for that reason again.
  • Reserve !important for genuinely exceptional cases: a utility that must win by definition, or overriding a third-party inline style you cannot remove. Write a comment saying which.
  • Never rely on source order across files you do not control the concatenation of. Bundlers reorder, code splitting changes arrival order, and a rule that only worked because it happened to come last is a time bomb (Code Splitting).
  • Prefer adding a layer or a state class over increasing specificity. Specificity is the step with no ceiling and no readability.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The important-user-origin band exists specifically so a user's own stylesheet — the mechanism behind browser and OS accessibility settings — can override an author. Anything you do that assumes you have the last word is fighting that (Contrast, Colour and Motion).
  • Forced-colors mode works by injecting user-agent declarations at a level your author rules cannot beat. Test in it rather than assuming your palette survives; the failure is silent and total for the people who need it.
  • Focus-visible styling loses cascade arguments constantly, usually to a broad reset with a later source order. A focus ring that exists in the stylesheet but never wins the cascade is the same as no focus ring (Keyboard Operability).
  • A cascade fight resolved by !important on a colour is frequently a contrast regression, because the winning value was chosen to end an argument rather than to be readable. Check the resulting pair, not the intent.
  • @media (prefers-reduced-motion: reduce) blocks are ordinary author rules and lose ordinary cascade fights. Put them last, or in a layer that wins, or they are decorative.

What can go wrong

Failure modes
  • The !important ratchet: two !important declarations now compete, and the winner is decided by layer order and then specificity among the important ones — which almost nobody reasons about, so the fix becomes an inline style, and then an inline style with !important.
  • A layer declared implicitly by first use rather than explicitly at the top of the entry point, so its position depends on which file the bundler put first.
  • Unlayered CSS beating everything in your carefully ordered layers. It is not a bug; unlayered normal declarations sit above all layered ones by design, and one stray global stylesheet outside the layer structure defeats the whole scheme.
  • A transition or animation "ignoring" your rule. Transition declarations sit at the very top of the sort while they are running; animation declarations sit above all normal author declarations. Your rule is not losing on specificity (Cheap and Expensive Animation).
  • Debugging in the Styles pane and forgetting that it shows the *cascade for that element*, not the whole picture — an inherited value comes from an ancestor and will not appear as a matched rule at all.
What can arrive out of order
  • Step 6 is document order, and with code splitting the order in which route chunks and their stylesheets arrive is decided by the network. Two rules that tie on every earlier step can resolve differently on different loads of the same page (Code Splitting).
  • A lazily-loaded component's stylesheet can arrive after the component has rendered, so the element is briefly styled by whatever else matched. Declaring layer order up front makes the final outcome deterministic even when arrival order is not.
  • During a deployment, an old tab can hold one release's stylesheet while fetching a chunk from the next. Nothing in the cascade protects you from that; content-hashed assets and a reload prompt do (Long-Lived Clients and Version Skew).
Security
  • The cascade is not a trust boundary. A third-party stylesheet participates in the same author origin as yours, and layer order is a coordination tool between cooperating authors, not a defence (Third-Party Scripts and the Supply Chain).
  • Injected inline styles beat your stylesheet rules by step 3, which is why an HTML-injection bug that "only" allows a style attribute is enough to overlay a fake control on a real one (Clickjacking and Framing).
  • CSP style-src without unsafe-inline removes the inline-style step for injected content — one of the few places where a policy directly closes a cascade-based attack (Content Security Policy).
  • Nothing in the cascade validates a value's provenance. A value that arrived from user input and reached a style attribute is applied exactly like one you wrote (Sanitization and Trusted HTML).
Misreads
  • "Specificity decides the cascade." Specificity is one of six comparisons, and by the time you reach it the interesting decisions have often already been made.
  • "!important wins." It wins its band. Among important declarations the layer order is reversed and specificity still applies, so two !important rules produce exactly the same argument one level up.
  • "Inline styles have infinite specificity." They are compared at a different step entirely. An !important stylesheet rule beats a normal inline style, which infinite specificity would not predict.
  • "Later always wins." Only among declarations that tied on all five earlier comparisons.
  • "Layers are just a naming convention." They are a real step in the sort, above specificity, and unlayered CSS sits above all of them for normal declarations.

Measuring it, and what changes in the field

How you would see this
  • The Styles pane in devtools is the cascade, rendered: matched rules in winning order, losing declarations struck through, inherited values in their own section. It is the primary instrument for this lesson (A Method for Frontend Bugs).
  • The Computed pane shows the winner per property and, expanded, the chain of declarations it beat — often faster than reading the Styles pane for a single stubborn property.
  • Recalculate Style in the Performance panel tells you how expensive the sort is in aggregate, and which change triggered it (Debugging Rendering and Jank).
  • A stylesheet linter can report !important counts and layer usage over time. The trend matters more than the number: rising !important density is a design signal, not a style-guide violation.
Slow device, slow network, large data, old tab
  • On a large document, the sort runs per element in the invalidation set, so a cascade that is fine on a settings page can be the dominant cost on a data grid (List Virtualization).
  • In a design system consumed by teams you do not control, layers become the contract. Without them, precedence is decided by whichever bundle happens to load last, which varies per route (Design Systems).
  • In a micro-frontend setup, several independently built stylesheets land in one document with no shared layer order at all. This is the environment where the cascade genuinely does break down, and shadow DOM or scoped naming becomes the answer (Micro Frontends).
  • On an old tab running a previous release alongside a new one, two versions of a stylesheet can coexist. Source order between them is not something you specified (Long-Lived Clients and Version Skew).
What this costs
  • Cascade layers make precedence explicit and cost you a mental model that half your team has not learned yet. The reversal of layer order under !important in particular surprises people the first several times.
  • Shadow DOM gives real encapsulation and takes away the ability to restyle a component from the page — which is a feature until a designer needs a one-off variant that the component did not expose.
  • Utility-first CSS mostly removes cascade arguments by making almost every declaration equally specific and last-wins. It relocates the complexity into class-name management and markup readability rather than eliminating it.
  • Banning !important outright means the genuinely exceptional cases get solved by something worse, usually an inline style written from JavaScript.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe six-step sorting order is specified in CSS Cascading and Inheritance, and Blink, Gecko and WebKit implement it identically. Disagreements about "which rule wins" between browsers are almost always a difference in what matched, not in how the cascade sorted.
  • SPEC-EVOLVINGThe sort has gained steps within recent memory: cascade layers were added in Cascading 5, and scope proximity from @scope slots in between layers and specificity in Cascading 6. Support for @scope lags layers across engines, so treat the position of proximity as current-spec rather than universally available, and check support before depending on it.
  • BROWSER-SPECIFICHow the sort is *surfaced* differs: Chrome groups matched rules by layer and shows layer names in the Styles pane, Firefox shows the layer as a separate annotation, and Safari's presentation of layered rules is less explicit — so a cascade bug can be much easier to see in one browser than another.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — precedence declared once at a boundary versus precedence that emerges from local decisions, which is the same argument as explicit dependency ordering in any layered system.