020: Cascade and Specificity
Learning outcomes
By the end of this lesson, you should be able to predict which declaration wins when rules conflict by working through origin, importance, layers, specificity, scoping proximity, and source order. You should also be able to recognize inherited values, explain how inline styles participate in the cascade, avoid reaching for !important, and investigate a conflict in DevTools.
Prerequisites and retrieval
Open the selector laboratory and rank these selectors from least to most specific: h2, .project h2, #projects h2. Also retrieve the difference between a descendant combinator and a compound selector. Those ideas are the starting point for understanding what the browser does when several matching rules set the same property.
Terminology
- Cascade: "The cascade sorts declarations by origin, importance, layer, specificity, and order, and selects the winning declaration (cascaded value) for each property on each element." — Source: CSS Cascading and Inheritance Level 5
- Origin: "Origin of a declaration — user-agent, user, or author (plus transitions and animations)." — Source: CSS Cascading and Inheritance Level 5: Origins
- Importance: "Whether a declaration is normal or important (!important)." — Source: CSS Cascading and Inheritance Level 5: Importance
- Cascade layer: An explicit
@layerprecedence group within an origin that orders competing rules. — Source: CSS Cascading L5: Layering - Specificity: "Specificity is a weight derived from the number of ID selectors, class selectors, and type selectors in a selector." — Source: Selectors Level 4: Specificity & MDN: Specificity
- Scoping proximity: When rules tie otherwise, the one closer to its
@scoperoot wins. — Source: CSS Cascading L5 - Order of appearance: The final tie-breaker: among equal candidates the later declaration wins. — Source: CSS Cascading L5: Cascade order
- Inheritance: "Some properties inherit computed values from their parent element when no cascaded value is specified." — Source: CSS Cascading and Inheritance Level 5: Inheritance
- Inline style: Declarations written in an element’s
styleattribute; they outrank normal author rules. — Source: CSS Cascading L5 - Computed value: The value produced after cascade and inheritance processing, used for inheritance downstream. — Source: CSS Cascading L5: Value processing
Mental model: a tournament, not “last rule wins”
Think about one element and one property at a time. First eliminate declarations that cannot participate: their selectors may not match, or a conditional rule may currently be false. The remaining declarations go through an ordered comparison: origin and importance, cascade-layer order within that origin, specificity, scoping proximity when @scope is involved, and finally order of appearance. “The last rule wins” is only the shortcut for the case where every earlier comparison is tied.
From low to high precedence, the major origin/importance groups are user-agent normal, user normal, author normal, CSS keyframe animations, author !important, user !important, user-agent !important, and active CSS transitions. Within an origin, normal declarations in later layers outrank earlier layers, while unlayered normal rules outrank layered normal rules. Important layer order reverses: earlier layers have priority, and layered important rules outrank unlayered important rules. These stages establish precedence before selector weight enters the comparison.
Most examples here use matching, unlayered, normal author rules with no @scope. In those examples, origin, importance, and layer position are tied, so the practical comparison starts with specificity and then reaches source order. That shortcut is safe only after you have checked that the earlier stages really do tie.
Specificity is compared by columns, not by adding up a single decimal score:
- IDs
- Classes, attributes, and pseudo-classes
- Types and pseudo-elements
Compare the columns from left to right. #projects (1-0-0) beats any number of classes without an ID. .project h3 is 0-1-1; article.project h3 is 0-1-2; .project.featured is 0-2-0. Combinators and * contribute nothing. Among normal author declarations at the same relevant cascade precedence, an inline style outranks declarations in style rules; an important stylesheet declaration can still outrank a normal inline style.
Inheritance is not another specificity contest. A directly matched declaration, even one from p, beats an inherited color from a highly specific parent rule. Text properties such as color, font-family, and line-height commonly inherit. Box properties such as margin, padding, border, and width generally do not.
Beginner example: predict deliberate conflicts
<section id="projects" class="portfolio-section">
<article class="project featured">
<h2 class="project-title">Library finder</h2>
<p>Search public libraries by location.</p>
<a class="project-link" href="#">View project</a>
</article>
</section>
body { color: rgb(51 65 85); }
h2 { color: rgb(2 132 199); } /* 0-0-1 */
.project-title { color: rgb(21 128 61); } /* 0-1-0 */
.project .project-title { color: rgb(180 83 9); }/* 0-2-0 */
h2 { color: rgb(190 24 93); } /* 0-0-1 */
.project-link { color: rgb(30 64 175); }
.project-link { text-decoration-thickness: 2px; }
Make a prediction before loading the page. The heading is amber (rgb(180 83 9)) because 0-2-0 beats both 0-1-0 and 0-0-1, even though the matching type rule appears later. The two .project-link rules do not compete: they set different properties, so both declarations apply. The paragraph has no direct color declaration and therefore inherits the body’s color.
Now remove .project .project-title. .project-title beats both type rules. Remove the class rule as well, and the second h2 wins because the two remaining selectors have equal specificity, leaving source order as the tie-breaker.
Add style="color: purple" to the heading. It beats the normal stylesheet rules, but remove it afterward. Inline styles put presentation in the markup and make later overrides awkward.
Intermediate example: fix a specificity trap
Here is a stylesheet that has started an escalation cycle:
#projects .project a { color: rgb(185 28 28); }
.project-link { color: rgb(29 78 216); }
.project-link { color: rgb(29 78 216) !important; }
The class selector cannot beat 1-1-1, so someone added !important. That fixes the immediate symptom while making the next override harder. A first repair is to remove the unnecessary ID-based styling and the !important:
.project a { color: rgb(185 28 28); } /* component default: 0-1-1 */
.project-link { color: rgb(29 78 216); } /* explicit variant: 0-1-0 */
This version still does not give the link the intended blue color: .project a remains more specific. A better design gives the base and its variant the same class-based API, or otherwise deliberately lowers the base specificity:
.project-link { color: rgb(185 28 28); }
.project-link--primary { color: rgb(29 78 216); }
<a class="project-link project-link--primary" href="#">View project</a>
Both selectors are 0-1-0, so the intentionally later variant wins. Predictable CSS comes from controlling selector weight, not from winning an arms race against the previous rule.
For an inherited link color, make the intent explicit:
.project { color: rgb(51 65 85); }
.project-link { color: inherit; }
inherit makes the child property use the parent’s computed value. initial uses the property’s specification-defined initial value; unset behaves like inherit for inherited properties and like initial otherwise; revert rolls back to an earlier origin. Use these keywords to express a deliberate choice, not as random repair attempts.
Optional advanced example: low-specificity defaults
:where() is broadly available and always contributes zero specificity, including the specificity of its arguments:
:where(.portfolio) h2 { color: rgb(51 65 85); } /* 0-0-1 */
.project-title { color: rgb(30 64 175); } /* 0-1-0 */
This makes a default easy to override. It is optional here; ordinary class selectors are enough for the portfolio. Cascade layers become valuable in larger systems, but adding layers is not a substitute for understanding the ordinary cascade.
Mistakes, debugging, and DevTools
- Assuming later always wins: check origin/importance and layer order before specificity, then check scoping proximity before source order.
- Counting a specificity total:
1-0-0is not “100” that enough classes can eventually exceed. - Counting inherited parent specificity against a child rule: inheritance loses to a direct match.
- Adding IDs to “strengthen” rules: this makes future overrides expensive.
- Using
!importantbefore finding the winner: it changes cascade order and can defeat user needs. Reserve it for constrained cases you can explain, such as overriding uneditable third-party important CSS. - Confusing a crossed-out declaration with invalid syntax: a crossed-out declaration usually lost to another declaration; a warning icon or missing declaration points more strongly to invalid CSS.
Inspect the heading. In Styles, find every color; crossed-out values are the candidates that lost. DevTools identifies the winning source file and line. Expand the computed color to see contributing rules. Toggle rules and edit selectors live. Check “inherited from body” separately. This gives you evidence about the conflict instead of encouraging a blind !important fix.
Accessibility and performance
The cascade includes user styles for a reason. Important user declarations can override important author declarations, which supports users who need larger text or different colors. Excessive author !important and rigid inline styles make that adaptation harder. Never remove focus outlines globally; if a reset has done that, restore a strong :focus-visible rule.
Low-specificity reusable CSS often means fewer duplicate overrides. Performance differences between ordinary selectors are negligible beside image, font, and network costs, but a specificity war grows the stylesheet and increases maintenance risk.
Deep dive: the cascade as an ordered decision process
When two declarations target the same property on the same element, resist the urge to jump straight to specificity. Work through this sequence:
- Relevance: Does the rule match the element under the current conditions?
- Origin and importance: Is it user-agent, user, or author CSS, and is it normal or important?
- Cascade layers: What precedence do the layered and unlayered rules have?
- Specificity: Which selector has the greater weight?
- Scoping proximity: Which rule is closer when
@scopeapplies? - Source order: Which declaration is later, if all earlier stages tie?
In day-to-day application CSS, most conflicts are between normal author declarations. That is why layers, specificity, and source order show up so often, but the earlier stages still explain the exceptions.
Specificity is a tuple, not a single mysterious score
Keep the columns visible:
- inline styles;
- IDs;
- classes, attributes, and pseudo-classes;
- type selectors and pseudo-elements.
Examples:
button {} /* 0-0-0-1 */
.button {} /* 0-0-1-0 */
button.button {} /* 0-0-1-1 */
#checkout .button {} /* 0-1-1-0 */
.card :where(h2, h3) {} /* .card contributes; :where() contributes 0 */
Do not turn specificity into decimal arithmetic such as “100 versus 10.” The tuple model preserves the fact that a higher column cannot be overtaken by accumulating values in a lower column.
Deep dive: inheritance is separate from the cascade
Some properties naturally inherit from an ancestor:
body {
color: #1f2937;
font-family: system-ui, sans-serif;
}
Unless a more specific value is supplied, descendants normally inherit color and font-family.
Many layout properties do not inherit:
.card {
padding: 1rem;
border: 1px solid #d1d5db;
}
The card’s children do not automatically receive its padding or border.
These global keywords are useful when their behavior matches your intent:
.example-a { color: inherit; }
.example-b { color: initial; }
.example-c { color: unset; }
.example-d { color: revert; }
inheritexplicitly takes the parent’s computed value.initialuses the property’s initial value from the specification.unsetacts likeinheritfor inherited properties and likeinitialfor non-inherited properties.revertrolls back to the value from an earlier cascade origin/layer context rather than merely using the property’s initial value.
Use these keywords when they communicate intent. Sprinkling them across a stylesheet is not a replacement for understanding why a value won.
Deep dive: cascade layers
Layers let a project establish precedence before selectors begin competing:
@layer reset, base, components, utilities;
@layer base {
a {
color: #1d4ed8;
}
}
@layer components {
.button {
color: white;
background: #2563eb;
}
}
@layer utilities {
.text-danger {
color: #b91c1c;
}
}
The layer order is explicit. That is particularly useful when a project combines resets, design-system styles, and third-party CSS. Layers do not erase specificity; they add an earlier ordering dimension so selectors compete within the appropriate priority zone.
Worked example: resolve a four-rule conflict
HTML:
<a id="buy" class="button featured" href="/buy">Buy now</a>
CSS:
a {
color: green;
}
.button {
color: blue;
}
a.featured {
color: purple;
}
#buy {
color: red;
}
All four declarations match. They also have the same origin and importance, so specificity makes the decision:
a-> type selector only.button-> one classa.featured-> one class plus one type#buy-> one ID
The computed color is red.
Now add:
.button {
color: orange !important;
}
The important author declaration moves into a different importance bucket and beats the normal ID declaration. This demonstrates why !important is not “infinite specificity”: it changes the cascade stage before specificity is compared.
Worked example: replace a specificity war with a stable API
Fragile:
#app main .checkout .summary .button.primary {
background: green;
}
.page.checkout-page main .checkout .summary button.button.primary {
background: darkgreen;
}
Better:
.checkout-action {
background: green;
}
.checkout-action:hover {
background: darkgreen;
}
If a variant is needed:
.checkout-action[data-tone="success"] {
background: green;
}
The goal is not to make selectors weak merely for the sake of it. The goal is to make ownership obvious and overrides intentional.
Debugging drill: read DevTools from winner backward
When a property is wrong:
- Inspect the element.
- Find the property in Computed.
- Expand it to see the winning declaration and overridden candidates.
- Identify whether the losing declaration lost at the layer, specificity, or source-order stage.
- Fix the ownership model instead of reflexively adding
!important.
A useful rule of thumb: if selector length keeps increasing just to make CSS work, stop and investigate the cascade.
Tiered exercises
Checkpoint: compute one conflict completely
When a declaration surprises you, write a small evidence table with columns for source, selector, match, importance, specificity, and order. Remove nonmatching rules immediately. Compare only declarations for the same property: a winning color tells you nothing about which margin wins. This keeps you from treating an entire rule as though it were simply victorious or defeated.
Refactoring is usually better than escalation. Find out why the heavier selector exists, reduce it to a stable component class where possible, and keep variants at equal weight after their defaults. After changing selectors, inspect ordinary, featured, and nested instances to catch scope regressions. The goal is not merely to produce blue text; it is to leave a stylesheet whose next override is unsurprising.
Keep cascade and inheritance separate during review as well. Inspect a child with no direct declaration and note its inherited computed value. Then add a matching type rule. Even a low-specificity direct match replaces the inherited value because inheritance fills an otherwise unresolved property; it does not join the selector contest. Repeat the test with margin and observe that a parent’s margin is not inherited. This contrast removes two common debugging myths at once.
Foundation: For each pair, state the winner: p versus .intro; .card a versus a; two identical .card rules in order.
Core: Given the broken CSS below, make the featured title blue without IDs, inline style, or !important, while ordinary titles remain slate.
.project h3 { color: slategray; }
#projects article.featured h3 { color: tomato; }
.featured-title { color: blue; }
Stretch: Create a low-specificity portfolio heading default with :where(), then override it with one class. Explain both specificity tuples.
Foundation: .intro beats p; .card a beats a; the later identical .card rule wins.
.project-title { color: rgb(71 85 105); }
.project-title--featured { color: rgb(29 78 216); }
:where(.portfolio) h3 { color: rgb(71 85 105); } /* 0-0-1 */
.project-title { color: rgb(29 78 216); } /* 0-1-0 */
<h3 class="project-title project-title--featured">Featured project</h3>
Remove the old #projects article.featured h3 rule instead of trying to overpower it.
Recap and exit questions
The cascade selects one value for each property through an ordered set of comparisons. Origin and importance, followed by layers, are resolved before specificity; scoping proximity and source order settle later ties. Specificity compares ID, class-like, and type-like columns. Inheritance supplies a value only when no direct declaration has won.
- When does source order decide a conflict?
- What is the specificity of
.project > a:hover? - Why does a direct
p { color: ... }beat color inherited from#app? - Why is
!importantusually a poor repair? - Which DevTools views reveal the winner and inherited values?
