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.
When two declarations both apply to an element, which one wins, and at which step of the sort did the other one lose?
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 more specific selector wins, and if that fails, !important wins. If neither works, add another class and try again.
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.
- 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
!importantrule. - 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.
!importantdoes not simply "win". It moves the declaration into a different importance band, and it reverses the ordering of cascade layers — so an!importantdeclaration 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
!importantadded to end an argument makes the next argument harder, and a codebase converges on a state where the only tool left is another!importantplus an inline style.
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
styleattribute 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!importantrule in a stylesheet still beats a normal inline style. - Step 4 — cascade layers. Within one origin,
@layerestablishes 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.
| # | Comparison | What wins | The failure it explains |
|---|---|---|---|
| 1 | Origin and importance | Transitions, then important UA, important user, important author, then animations, then normal author, normal user, normal UA | Your rule losing to a user stylesheet or to forced-colors mode; a value refusing to change during a transition |
| 2 | Context (shadow trees) | Outer tree for normal declarations; inner tree for important ones | A page style failing to reach into a web component, or a component's important rule refusing to be overridden |
| 3 | Element-attached styles | The style attribute beats stylesheet rules in the same band | A framework or animation library writing inline styles that your stylesheet cannot beat |
| 4 | Cascade layers | Unlayered beats layered for normal; the order reverses for important | A utility class losing to a component rule, or an !important in the "strongest" layer suddenly being the weakest |
| 5 | Specificity | The higher (a, b, c) tuple (Specificity) | The classic override fight, and the only step with no natural ceiling |
| 6 | Order of appearance | The last declaration in document order | Two 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.
/* their rule */
#datepicker .dp-cell.dp-selected { background: #6b7280; }
/* ours, third attempt */
#app #datepicker .dp-cell.dp-selected.is-active {
background: var(--brand) !important;
}@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.
1/* First thing in the entry stylesheet. Declaring the order up front means2 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.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Rule is struck through, and the winner has a much weaker selector | A one-class rule beats your three-class rule | Layer order, or the winner is unlayered while yours is in a layer | Check the layer annotation in the Styles pane before counting anything (A Mental Model of the Devtools). |
Winner shows as element.style | No stylesheet rule can beat it | Step 3 — an inline style, usually written by a framework, an animation library or a drag handler | Fix 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 runs | Devtools shows your rule as the winner, the screen disagrees | Step 1 — animation and transition declarations sit above normal author declarations | Change 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 screenshot | High-contrast palette, your brand colours gone | Step 1 — forced-colors injects user-agent declarations you are not meant to beat | Design 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 wins | Works locally, breaks after a build or on one route only | Step 6 — source order, decided by the bundler or by which chunk loaded first | Make it explicit with a layer. Relying on emitted order is relying on a build detail (Code Splitting). |
Two !important declarations both apply | The escalation stopped working | Among important declarations, layer order is reversed and specificity still decides ties | Remove 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
@layerto 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 intovendormeans your normal rules beat theirs regardless of how many ids they used, and you never write!importantfor that reason again. - Reserve
!importantfor 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
!importanton 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
- The
!importantratchet: two!importantdeclarations 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.
- 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).
- 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
styleattribute is enough to overlay a fake control on a real one (Clickjacking and Framing). - CSP
style-srcwithoutunsafe-inlineremoves 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
styleattribute is applied exactly like one you wrote (Sanitization and Trusted HTML).
- "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.
- "
!importantwins." It wins its band. Among important declarations the layer order is reversed and specificity still applies, so two!importantrules produce exactly the same argument one level up. - "Inline styles have infinite specificity." They are compared at a different step entirely. An
!importantstylesheet 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
- 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
!importantcounts and layer usage over time. The trend matters more than the number: rising!importantdensity is a design signal, not a style-guide violation.
- 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).
- 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
!importantin 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
!importantoutright 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
@scopeslots in between layers and specificity in Cascading 6. Support for@scopelags 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.
- — 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.