020: Cascade and Specificity
Learning outcomes
By the end, you can predict conflicting declarations using cascade origins, importance, layers, specificity, scoping proximity, and order; identify inherited values; explain inline styles; avoid !important; and diagnose a conflict in DevTools.
Prerequisites and retrieval
Open the selector laboratory. Rank these from least to most specific: h2, .project h2, #projects h2. Retrieve the difference between a descendant combinator and a compound selector. Today's work explains what happens 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 @layer precedence 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 @scope root 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 style attribute; 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”
For one element and one property, first discard irrelevant declarations, including selectors that do not match and conditional rules whose conditions are false. The cascade then compares origin and importance, cascade-layer order within each origin, specificity, scoping proximity where @scope is involved, and finally order of appearance. “The last rule wins” is true only when every earlier comparison ties.
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, and unlayered normal rules outrank layered normal rules. Important layer order reverses so earlier layers have priority, and layered important rules outrank unlayered important rules. These details let origins, importance, and layers establish precedence before selector weight is considered.
Most examples in this lesson use matching, unlayered, normal author rules without @scope. Because their origin, importance, and layer position tie, their practical comparison starts at specificity and then reaches order of appearance. That simplified workflow is useful only after confirming those earlier stages tie.
Specificity is compared as columns, not one decimal score:
- IDs
- Classes, attributes, and pseudo-classes
- Types and pseudo-elements
Compare 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 * add nothing. Among normal author declarations at the same relevant cascade precedence, an inline style outranks style-rule declarations; an important stylesheet declaration can still outrank a normal inline style.
Inheritance is not a specificity contest. A directly matched declaration, even 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; }
Before loading, predict the heading. It is amber (rgb(180 83 9)) because 0-2-0 beats all 0-1-0 and 0-0-1 rules regardless of their later position. The two .project-link rules do not conflict because they set different properties; declarations combine. The paragraph inherits the body's color.
Now remove .project .project-title. .project-title wins over both type rules. Remove the class rule too. The second h2 wins because equal specificity reaches the source-order tiebreaker.
Add style="color: purple" to the heading. It beats normal stylesheet rules, but remove it afterward. Inline styles hide presentation in markup and are awkward to override.
Intermediate example: fix a specificity trap
Problem stylesheet:
#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 cannot beat 1-1-1, so somebody added !important. That solves today's symptom and creates tomorrow's escalation. The minimal repair is to remove unnecessary ID-based styling and !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 still does not work: .project a is more specific. Better design the base through the same class or lower its 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 intentional later variant wins. Predictable CSS comes from controlling selector weight, not winning arms races.
For inherited link color, be explicit:
.project { color: rgb(51 65 85); }
.project-link { color: inherit; }
inherit forces the child property to take the parent's computed value. initial uses the property's specification-defined initial value; unset behaves as inherit for inherited properties and initial otherwise; revert rolls back to an earlier origin. Use these deliberately, not as random fixes.
Optional advanced example: low-specificity defaults
:where() is broadly available and always has zero specificity, including its arguments:
:where(.portfolio) h2 { color: rgb(51 65 85); } /* 0-0-1 */
.project-title { color: rgb(30 64 175); } /* 0-1-0 */
It can create easily overridden defaults. This is optional: simple class selectors are enough for the portfolio. Cascade layers are useful in larger systems, but do not add them merely to avoid understanding ordinary cascade rules.
Mistakes, debugging, and DevTools
- Assuming later always wins: compare origin/importance and layer order before specificity, then scoping proximity before order of appearance.
- Counting a specificity total:
1-0-0is not “100” that many 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 costly.
- 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: crossed out usually means another declaration won; a warning icon or missing declaration suggests invalid CSS.
Inspect the heading. In Styles, find every color; crossed-out values lost. DevTools labels the winning source file and line. Expand Computed color to see contributing rules. Toggle rules and edit selectors live. Check “inherited from body” separately. This workflow is faster and safer than adding !important blindly.
Accessibility and performance
The cascade includes user styles for a reason. Important user declarations can override important author declarations, supporting users who need larger text or different colors. Excessive author !important and rigid inline styles make adaptation harder. Do not globally remove focus outlines; if a reset does, restore a strong :focus-visible rule.
Low-specificity reusable CSS usually ships fewer duplicate overrides. Performance differences among normal selectors are negligible compared with image, font, and network costs, but a specificity war increases stylesheet size and maintenance risk.
Deep dive: the cascade as an ordered decision process
When two declarations target the same property on the same element, do not jump directly to specificity. Use this sequence:
- Relevance: Does the rule match the element and current conditions?
- Origin and importance: user-agent, user, author; normal versus important declarations.
- Cascade layers: unlayered and layered author rules have defined precedence.
- Specificity: compare selector weight.
- Scoping proximity where applicable.
- Source order: later wins only when earlier stages tie.
For day-to-day application CSS, most conflicts are among normal author declarations, so layers, specificity, and source order matter most.
Specificity is a tuple, not a single mysterious score
Think in columns:
- inline styles;
- IDs;
- classes, attributes, 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 convert specificity into decimal arithmetic such as “100 versus 10.” The tuple model prevents incorrect reasoning.
Deep dive: inheritance is separate from the cascade
Some properties naturally inherit:
body {
color: #1f2937;
font-family: system-ui, sans-serif;
}
Descendants normally inherit color and font-family unless a more specific value is specified.
Many layout properties do not inherit:
.card {
padding: 1rem;
border: 1px solid #d1d5db;
}
Children do not automatically receive the card's padding or border.
Useful global keywords:
.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.unsetbehaves likeinheritfor inherited properties andinitialfor non-inherited properties.revertrolls back to the value from an earlier cascade origin/layer context rather than merely the property's initial value.
Use these when they express intent; do not spray them over a stylesheet as a substitute for understanding the cascade.
Deep dive: cascade layers
Layers let a project define precedence before selector battles begin:
@layer reset, base, components, utilities;
@layer base {
a {
color: #1d4ed8;
}
}
@layer components {
.button {
color: white;
background: #2563eb;
}
}
@layer utilities {
.text-danger {
color: #b91c1c;
}
}
Layer order is explicit. This is especially helpful when integrating resets, design-system styles, and third-party CSS. Layers do not make specificity disappear; they provide an earlier ordering dimension so that selectors only compete inside 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 are relevant. They share the same origin and importance. Specificity decides:
a→ type selector only.button→ one classa.featured→ one class + 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 wins over the normal ID declaration. This is 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” for its own sake. 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.
- Note whether the loss came from layer, specificity, or source order.
- Fix the ownership model rather than adding
!importantreflexively.
A good rule of thumb: if you need to keep increasing selector length 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 says nothing about which margin wins. This prevents the common mistake of treating a whole rule as victorious or defeated.
Refactoring is usually better than escalation. Find why the heavier selector exists, reduce it to a stable component class when possible, and keep variants at equal weight after defaults. After changing selectors, inspect several ordinary, featured, and nested instances to catch scope regressions. The desired result is not merely blue text; it is a stylesheet whose next override remains unsurprising.
Also separate cascade from inheritance during review. Inspect a child with no direct declaration, 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 enter the selector contest. Repeat with margin and observe that no parent margin is inherited. This contrast prevents 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 rather than attempting to overpower it.
Recap and exit questions
The cascade selects one value per property through ordered comparisons. Origin/importance and layers are resolved before specificity; scoping proximity and order of appearance break later ties. Specificity compares ID, class-like, and type-like columns. Inheritance supplies values only where no direct winning declaration does.
- 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?
