CSS Module 3 – The cascade, specificity and layers
The cascade: which declaration wins
How a browser picks one value when CSS declarations conflict: origins and importance, shadow trees, the style attribute, layers, specificity and order.
What you will learn
- List the cascade's sorting steps in order and say which step decides a given conflict
- Predict the winner when browser, user and author styles conflict
- Explain how !important reverses origin order and why it is a last resort
Before you start
What you’ll learn · Before you start
What you will learn
- List the cascade's sorting steps in order and say which step decides a given conflict
- Predict the winner when browser, user and author styles conflict
- Explain how !important reverses origin order and why it is a last resort
Before you start
On this page
When two or more declarations give the same property different values on the same element, the browser keeps
exactly one of them, and the process that chooses it is the cascade. It compares the competing declarations by
asking six questions, always in the same order: where the declaration comes from and whether it is marked
!important; whether it comes from the page or from a shadow tree; whether it sits in the element’s own style
attribute; which cascade layer holds it; how specific its selector is; and which one appears last. The first
question that tells two declarations apart decides between them, and the later questions are never asked. That
order explains most “my CSS is being ignored” bugs. Specificity is only the fifth question, so a heavy selector can
lose to a light one in a later layer, to a style attribute, or to any !important declaration. The rules come
from CSS Cascading and Inheritance Level 5, and every conflict below is decided by a small cascade model whose
output is recorded on this page.
What the cascade compares
Before anything is sorted, the browser collects the declared values for each property of each element. A
declaration takes part only if its style sheet applies to the document, it is not inside an @media or @supports
rule whose condition is false, its rule’s selector matches the element, and it is valid: the property name is
known and the value fits that property’s grammar. A declaration such as color: 12px is thrown away while the
style sheet is read, so it never competes and can never win.
The cascade works on single declarations, not on whole rules. In the pair below, the second rule wins color and
has nothing to say about padding, so the card keeps the first rule’s padding:
/* Both rules match <div id="main"><p class="card">…</p></div>. */
.card {
color: red;
padding: 1rem;
}
#main .card {
color: blue;
}
/* Result: color comes from the second rule, which is more specific;
padding comes from the first, because no other rule sets it. */ The cascade’s output is the list of declared values sorted from winner to loser. The first entry is the cascaded value. When no declaration applies at all, there is no cascaded value, and the property is filled in by inheritance or takes its initial value, which is the subject of the lesson on inheritance and computed values.
The six questions, in order
The cascade asks six questions, in this order
Text description of the diagram
The diagram shows the six questions the cascade asks about two declarations of the same property on the same element, from top to bottom. The first question that separates them decides the winner; a tie sends the pair to the next question.
- Origin and importance: which origin the declaration comes from, and whether it is marked !important.
- Context: whether it comes from the page or from a shadow tree inside it.
- Style attribute: whether it is written in the element's own style attribute.
- Layers: which cascade layer it belongs to.
- Specificity: whose selector has the higher specificity.
- Order of appearance: which declaration comes last.
- Origin and importance. Declarations from the browser, the reader and the site rank differently, and
!importantchanges that ranking. This question settles more conflicts than any other. - Context. A web component’s shadow tree is a separate context. For normal declarations the page’s rule
beats the component’s own; for
!importantones the component’s rule wins. - Style attribute. A declaration written in
style="…"on the element beats every declaration of the same importance that reaches the element through a selector, however specific that selector is. - Layers. Rules inside
@layerblocks rank by the order of their layers, and rules outside every layer count as one more layer after all the others. - Specificity. The selector with more IDs wins; on a tie, the one with more classes, attributes and pseudo-classes; then the one with more type selectors and pseudo-elements.
- Order of appearance. If everything else is equal, the declaration that comes later in the style sheets wins.
Two declarations that differ on question 1 are never compared on specificity. This is the misunderstanding behind the advice “use a more specific selector”: it works only when the competing declarations already tie on origin, importance, context, attachment and layer.
Origins: browser, reader and site
Every declaration has an origin, the place it enters the cascade from.
- The user-agent origin is the browser’s default style sheet. The HTML Standard lists the styles a browser is
expected to apply, such as
li { display: list-item; }, a rule that gives elements with thehiddenattributedisplay: none, andinput[type=hidden i] { display: none !important; }. - The user origin holds styles the reader asks for: a user style sheet, an extension that injects one, or a browser setting that behaves like one.
- The author origin is the site: its linked style sheets, its
<style>elements and itsstyleattributes.
CSS Animations and CSS Transitions add two more origins whose declarations exist only while an animation or a transition runs.
!important turns the order around
For normal declarations the site beats the reader, and the reader beats the browser. Marking a declaration
!important raises it above every normal declaration and reverses the order of the three origins among important
ones. The program below gives color one declaration from each origin, normal and important, and lets the cascade
model in css-model/cascade.mjs sort them:
// One declaration of `color` from each origin, normal and !important, sorted by the cascade model in
// ./css-model/cascade.mjs. Everything else about them is the same, so only origin and importance can decide.
import { cascade, originOf } from "./css-model/cascade.mjs";
const declarations = [
{ origin: "user-agent", important: false, value: "black" },
{ origin: "user-agent", important: true, value: "gray" },
{ origin: "user", important: false, value: "navy" },
{ origin: "user", important: true, value: "yellow" },
{ origin: "author", important: false, value: "teal" },
{ origin: "author", important: true, value: "crimson" },
{ origin: "animation", value: "orange" },
{ origin: "transition", value: "purple" },
];
cascade(declarations).forEach((d, i) => {
console.log(`${i + 1}. ${originOf(d).padEnd(22)} color: ${d.value}`);
}); Output
1. transition color: purple 2. important user-agent color: gray 3. important user color: yellow 4. important author color: crimson 5. animation color: orange 6. normal author color: teal 7. normal user color: navy 8. normal user-agent color: black
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node origins.mjs
Read the result from the top. A running transition wins over everything, then come the important declarations of the browser, the reader and the site, in that order, then a running animation, then the normal declarations of the site, the reader and the browser.
The reversal is deliberate. The site normally decides how its pages look, but a reader who needs large text or
particular colours can insist with !important in their own style sheet, and no rule of the site can override
them, not even an important one. Above that, the browser can insist on the few things that must always hold: a
hidden input is never shown, whatever a site says about it.
Animations sit between the two halves. While an animation runs, its value beats every normal declaration, which is
why a keyframe can move an element that a selector-based rule tried to keep still. Any !important
declaration still beats it, and inside @keyframes a declaration marked !important is invalid and ignored, so an
animation cannot climb above important rules.
Ten conflicts, decided step by step
The program below builds a small page for each conflict, with a browser style sheet, a reader’s style sheet and the
site’s style sheet where the case needs them, and asks the model for the winning declaration and the question that
decided it. The model reads only the CSS these cases use; its six files are in the css-model folder beside the
example, and cascade.mjs, shown after the output, is the part that sorts.
// Ten conflicts, decided by the cascade model in ./css-model. Each case is a tiny page with its style sheets; the
// model prints the winning declaration, the one it beat, and the question of the cascade that decided between them.
import { decide } from "./css-model/cascade.mjs";
import { createPage } from "./css-model/index.mjs";
const cases = [
{
title: "The hidden attribute against a class",
html: `<div class="card" hidden>Weekly digest</div>`,
ua: `[hidden]:not([hidden=until-found i]):not(embed) { display: none }`,
author: `.card { display: flex }`,
element: ".card",
property: "display",
},
{
title: "An important browser rule against an important author rule",
html: `<input type="hidden" class="debug">`,
ua: `input[type=hidden i] { display: none !important }`,
author: `.debug { display: block !important }`,
element: ".debug",
property: "display",
},
{
title: "A reader's style sheet against the site's",
html: `<p class="fine-print">Terms apply</p>`,
user: `p { font-size: 24px !important }`,
author: `.fine-print { font-size: 12px !important }`,
element: ".fine-print",
property: "font-size",
},
{
title: "A style attribute against an ID selector",
html: `<p id="intro" style="color: purple">Welcome</p>`,
author: `#intro { color: red }`,
element: "#intro",
property: "color",
},
{
title: "An important rule against a style attribute",
html: `<p id="intro" style="color: purple">Welcome</p>`,
author: `#intro { color: red !important }`,
element: "#intro",
property: "color",
},
{
title: "A class in a later layer against an ID in an earlier layer",
html: `<p id="intro" class="lede">Welcome</p>`,
author: `@layer base, components;
@layer components { .lede { color: green } }
@layer base { #intro { color: red } }`,
element: ".lede",
property: "color",
},
{
title: "Two selectors in the same layer",
html: `<article class="card"><h2 class="title">News</h2></article>`,
author: `.card .title { color: teal }
.title { color: maroon }`,
element: ".title",
property: "color",
},
{
title: "Equal specificity: the later rule wins",
html: `<button class="btn danger">Delete</button>`,
author: `.danger { background-color: crimson }
.btn { background-color: navy }`,
element: ".btn",
property: "background-color",
},
];
const report = (n, title, winner, loser, step) => {
console.log(`${String(n).padStart(2)}. ${title}`);
console.log(` wins: ${winner}`);
console.log(` loses: ${loser}`);
console.log(` decided by: ${step}`);
};
cases.forEach((c, i) => {
const sheets = [
{ origin: "user-agent", css: c.ua ?? "" },
{ origin: "user", css: c.user ?? "" },
{ origin: "author", css: c.author ?? "" },
];
const page = createPage({ html: c.html, sheets });
const { winner, runnerUp, step } = page.explain(page.query(c.element), c.property);
report(i + 1, c.title, winner.source, runnerUp.source, step);
});
// Cases 9 and 10 need a shadow tree, which the page model does not build, so their declarations are written out:
// a component's own rule inside its shadow tree (context 1) and the page's rule for the same part (context 0).
for (const important of [false, true]) {
const flag = important ? " !important" : "";
const inner = { origin: "author", important, context: 1, specificity: [1, 0, 0], order: 1, source: `in the shadow tree: #label { color: gray${flag} }` };
const outer = { origin: "author", important, context: 0, specificity: [0, 0, 2], order: 2, source: `on the page: user-card::part(label) { color: black${flag} }` };
const { winner, loser, step } = decide(inner, outer);
report(important ? 10 : 9, `A shadow tree's rule against the page's rule${important ? ", both !important" : ""}`, winner.source, loser.source, step);
} Output
1. The hidden attribute against a class
wins: .card { display: flex }
loses: user-agent: [hidden]:not([hidden=until-found i]):not(embed) { display: none }
decided by: origin and importance
2. An important browser rule against an important author rule
wins: user-agent: input[type=hidden i] { display: none !important }
loses: .debug { display: block !important }
decided by: origin and importance
3. A reader's style sheet against the site's
wins: user: p { font-size: 24px !important }
loses: .fine-print { font-size: 12px !important }
decided by: origin and importance
4. A style attribute against an ID selector
wins: style="color: purple"
loses: #intro { color: red }
decided by: style attribute
5. An important rule against a style attribute
wins: #intro { color: red !important }
loses: style="color: purple"
decided by: origin and importance
6. A class in a later layer against an ID in an earlier layer
wins: @layer components { .lede { color: green } }
loses: @layer base { #intro { color: red } }
decided by: layers
7. Two selectors in the same layer
wins: .card .title { color: teal }
loses: .title { color: maroon }
decided by: specificity
8. Equal specificity: the later rule wins
wins: .btn { background-color: navy }
loses: .danger { background-color: crimson }
decided by: order of appearance
9. A shadow tree's rule against the page's rule
wins: on the page: user-card::part(label) { color: black }
loses: in the shadow tree: #label { color: gray }
decided by: context
10. A shadow tree's rule against the page's rule, both !important
wins: in the shadow tree: #label { color: gray !important }
loses: on the page: user-card::part(label) { color: black !important }
decided by: context
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node conflicts.mjs
// The cascade of CSS Cascading and Inheritance Level 5 (§6.1): the questions a browser asks, in order, when several
// declarations set the same property on the same element. The first question on which two declarations differ
// decides between them; the later questions are never asked.
//
// A declaration here is a plain object:
// { value, origin, important, context, attached, layer, specificity, order, source }
// origin "user-agent", "user", "author", "animation" or "transition"
// important true for a declaration marked !important
// context 0 for the document, 1 for a shadow tree inside it, 2 for one inside that, and so on
// attached true for a declaration in a style attribute
// layer the position of its cascade layer in the layer order (unlayered declarations come last)
// specificity [A, B, C] of the rule's selector
// order its position in the style sheets, in the order the browser reads them
/** Origin and importance, highest first (§6.1). */
export const PRECEDENCE = [
"transition",
"important user-agent",
"important user",
"important author",
"animation",
"normal author",
"normal user",
"normal user-agent",
];
export const originOf = (d) => (d.origin === "transition" || d.origin === "animation" ? d.origin : `${d.important ? "important" : "normal"} ${d.origin}`);
const rank = (d) => PRECEDENCE.length - PRECEDENCE.indexOf(originOf(d));
const compareSpecificity = (x, y) => x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
/**
* The cascade's questions in order. Each returns a positive number when declaration a wins, a negative one when b
* wins, and 0 when the question does not separate them.
*/
export const STEPS = [
["origin and importance", (a, b) => rank(a) - rank(b)],
// Normal declarations: the outer context wins. Important ones: the inner context wins.
["context", (a, b) => (a.important ? 1 : -1) * ((a.context ?? 0) - (b.context ?? 0))],
["style attribute", (a, b) => Number(!!a.attached) - Number(!!b.attached)],
// Normal declarations: the later layer wins. Important ones: the earlier layer wins.
["layers", (a, b) => (a.attached || b.attached ? 0 : (a.important ? -1 : 1) * ((a.layer ?? Infinity) - (b.layer ?? Infinity)) || 0)],
["specificity", (a, b) => (a.attached || b.attached ? 0 : compareSpecificity(a.specificity ?? [0, 0, 0], b.specificity ?? [0, 0, 0]))],
["order of appearance", (a, b) => (a.order ?? 0) - (b.order ?? 0)],
];
/** Which of two declarations wins, and the question that decided it. */
export function decide(a, b) {
for (const [step, test] of STEPS) {
const result = test(a, b);
if (result > 0) return { winner: a, loser: b, step };
if (result < 0) return { winner: b, loser: a, step };
}
return { winner: a, loser: b, step: "nothing (they are the same declaration)" };
}
/** Sorts declarations from the winner down: the cascade's output (§6.1). */
export function cascade(declarations) {
return [...declarations].sort((a, b) => (decide(a, b).winner === a ? -1 : 1));
} The model is small, but on these pages it agrees with real browser engines: apart from case 3, which needs a reader’s own style sheet, Chromium 153, Firefox 155 and WebKit 26.6 give the same winner in every case.
The cases that surprise people most are these.
- Case 1 is a real and common bug. A designer adds
hiddento a card, the card stays on screen, and the reason is the origin: the browser’s rule for[hidden], copied in the case from the HTML Standard, is a normal user-agent declaration, and the site’s.card { display: flex }is a normal author declaration, which outranks it whatever the selectors are. Bootstrap’s Reboot style sheet, for one, adds[hidden] { display: none !important; }to the site’s own CSS for exactly this reason. - Case 2 shows the other end of the ladder: an important browser rule beats an important site rule, so a hidden input cannot be shown with CSS at all.
- Cases 4 and 5 show the style attribute sitting between the two importance levels. It beats an ID selector,
because question 3 comes before specificity, and it loses to any
!importantrule of the site, because question 1 comes first. - Case 6 is the reason cascade layers exist: a one-class selector beats an ID selector because its layer comes later, and specificity never gets a say.
- Cases 9 and 10 involve a web component. Its shadow tree styles the inner
#labelelement; the page styles the same element throughuser-card::part(label), a pseudo-element the component exposes on purpose. For normal declarations the page wins, so a component’s internal styles act as defaults that the page can change. For!importantdeclarations the component wins, so a component can insist on something the page cannot undo.
Why !important is a last resort
!important is not a stronger kind of specificity. It moves a declaration to a different rung of the first question,
and once it is there, only another important declaration can beat it, decided by the same questions all over again.
A codebase that answers one !important with another ends up comparing important declarations by specificity and
order, exactly the fight it was trying to escape, and every later fix needs !important too.
There are good uses. A rule whose whole job is to override, such as a .visually-hidden utility or a [hidden]
reset, can reasonably be important, and a reader’s own style sheet is the reason the feature exists. For ordinary conflicts,
the questions that come after origin are better tools:
- lower the specificity of the rule that should lose, for example by replacing an ID with a class or by wrapping
part of a selector in
:where(), which counts for nothing; - put the two rules in cascade layers in the order you want them to win;
- move the rule that should win later in the source, when the specificities are equal.
Order of appearance, across files
The last question compares positions in the combined style sheets. The browser reads style sheets in the order the
document links them, <link> and <style> elements alike, and an @import counts as if the imported file were
pasted where the @import rule stands. Declarations in style attributes count as coming after every style sheet,
although question 3 has usually decided before position matters. Two consequences follow. Moving a <link> element
can change how a page looks without any CSS changing, and in a component library the order of the files is part of
its interface: a modifier class written before its base class loses every tie.
Version note
CSS Cascading and Inheritance Level 6 adds one more question between specificity and order of appearance:
scope proximity. When two rules come from different @scope blocks, the one whose scope root is fewer steps up
the tree from the element wins. The module on custom properties and architecture covers @scope.
Exercise: let the dark theme win
The panels below were styled years before anyone added a dark theme. The theme’s title and note should be white and silver, but they stay navy and slate grey.
exercise/component.css
/* Panels, written first. */
#app .panel .panel-title { color: navy; }
#app .panel .panel-note { color: slategray; }
/* The dark theme, added later: its title and note should be white and silver. */
.theme-dark { background-color: black; }
.theme-dark .panel-title { color: white; }
.theme-dark .panel-note { color: silver; } exercise/page.html
<main id="app">
<section class="panel">
<h2 class="panel-title">Orders</h2>
<p class="panel-note">3 waiting</p>
</section>
<section class="panel theme-dark">
<h2 class="panel-title">Payments</h2>
<p class="panel-note">All settled</p>
</section>
</main> Change only component.css so that every sample test passes, without !important and without any selector more
specific than (0, 2, 0). check.mjs reads the page and the style sheet with the same cascade model; run it with
node check.mjs in the exercise folder after each change, or paste the two files into the playground below and read
the computed colours in DevTools. As given, it fails three of its seven tests and says which rule won each failed
one and why:
// Sample tests for component.css: change the style sheet, then run `node check.mjs` in this folder.
// The page and the style sheet are read by the cascade model in ../css-model, which knows the CSS this exercise needs.
import { readFileSync } from "node:fs";
import { createPage } from "../css-model/index.mjs";
import { compareSpecificity, formatSpecificity, specificity } from "../css-model/selectors.mjs";
import { parseStyleSheet } from "../css-model/stylesheet.mjs";
const css = readFileSync("component.css", "utf8");
const page = createPage({ html: readFileSync("page.html", "utf8"), sheets: [{ origin: "author", css }] });
const light = "section:not(.theme-dark)";
const dark = "section.theme-dark";
const expectations = [
["the light panel's title is navy", `${light} .panel-title`, "color", "navy"],
["the light panel's note is slategray", `${light} .panel-note`, "color", "slategray"],
["the dark panel is black", dark, "background-color", "black"],
["the dark panel's title is white", `${dark} .panel-title`, "color", "white"],
["the dark panel's note is silver", `${dark} .panel-note`, "color", "silver"],
];
let failed = 0;
const result = (ok, name, detail = "") => {
if (!ok) failed++;
console.log(`${ok ? "pass" : "FAIL"} ${name}${detail}`);
};
for (const [name, selector, property, want] of expectations) {
const el = page.query(selector);
const got = page.computed(el, property);
if (got === want) {
result(true, name);
continue;
}
const { winner, runnerUp, step } = page.explain(el, property);
const why = winner && runnerUp ? `; ${winner.source} beat ${runnerUp.source} on ${step}` : "";
result(false, name, `: got ${got}${why}`);
}
const declarations = parseStyleSheet(css);
const important = declarations.filter((d) => d.important);
result(important.length === 0, "no declaration uses !important", important.length ? `: ${important.length} do` : "");
const limit = [0, 2, 0];
const tooStrong = [...new Set(declarations.flatMap((d) => d.selectors).filter((s) => compareSpecificity(specificity(s), limit) > 0).map((s) => `${s.text} ${formatSpecificity(specificity(s))}`))];
result(tooStrong.length === 0, `no selector is more specific than ${formatSpecificity(limit)}`, tooStrong.length ? `: ${tooStrong.join(", ")}` : "");
console.log(`${expectations.length + 2 - failed} of ${expectations.length + 2} tests pass`);
process.exitCode = failed ? 1 : 0; Output (exit status 1)
pass the light panel's title is navy
pass the light panel's note is slategray
pass the dark panel is black
FAIL the dark panel's title is white: got navy; #app .panel .panel-title { color: navy } beat .theme-dark .panel-title { color: white } on specificity
FAIL the dark panel's note is silver: got slategray; #app .panel .panel-note { color: slategray } beat .theme-dark .panel-note { color: silver } on specificity
pass no declaration uses !important
FAIL no selector is more specific than (0, 2, 0): #app .panel .panel-title (1, 2, 0), #app .panel .panel-note (1, 2, 0)
4 of 7 tests pass
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node check.mjs
First hint: the theme’s rules already come later, so they win as soon as the two sides tie on specificity. Which part of the panel selectors makes them stronger than they need to be?
HTML, CSS & JS Playground (Online Editor) Paste the page and the style sheet to see the conflict live, and watch DevTools' Styles panel cross out the losing declarations.Self-check
Check yourself
7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
What steps does the cascade use to decide which declaration wins? It compares declarations for the same property
on the same element in a fixed order: origin and importance, then context (the page or a shadow tree), then whether
the declaration is in the element’s style attribute, then cascade layers, then specificity, and finally order of
appearance. The first step where two declarations differ decides, so specificity matters only when origin,
importance, context, style attribute and layer all tie. Among normal declarations the site beats the reader and
the reader beats the browser; !important reverses that order and puts every important declaration above normal
ones.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A ten-year-old stylesheet has 140 !important declarations, and every new fix adds another. How do you remove them without visual regressions, and which ones would you keep?
Key takeaways
- The cascade sorts the valid declarations for one property on one element; invalid declarations never compete.
- Six questions in a fixed order: origin and importance, context, style attribute, layers, specificity, order of appearance. The first difference decides.
- Normal declarations: site over reader over browser. Important declarations: browser over reader over site, all above normal ones; transitions top everything and animations sit between the two halves.
- A
styleattribute beats any selector of the same importance and loses to any!importantrule of the site. !importantis a move to another rung, not a bigger specificity; prefer lower specificity, layers or source order for ordinary conflicts.- The browser’s rule that hides
[hidden]elements is a normal declaration, so any site rule that setsdisplayon the element defeats it.
References
- CSS Cascading and Inheritance Level 5: §5 Filtering, §6 Cascading, §6.3 Important declarations (W3C CSS Working Group)
- CSS Cascading and Inheritance Level 6: §2.1 Cascade sorting order (scope proximity) (W3C CSS Working Group)
- CSS Animations Level 1: §2 Animations, §3 Keyframes (W3C CSS Working Group)
- CSS Shadow Parts: the ::part() pseudo-element (W3C CSS Working Group)
- HTML Standard: §15.3 Non-replaced elements (the expected browser style sheet) (WHATWG)
- Bootstrap 5.3: Reboot, HTML5 [hidden] attribute (Bootstrap)
- Introduction to the CSS cascade (Mozilla (MDN Web Docs))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress