5.0 Overview
The cascade is powerful but becomes unpredictable as a project grows. Multiple
stylesheets, third-party libraries, and component styles compete for the same elements,
and the rule with the highest specificity wins regardless of where it appears in source
order. The strategies in this chapter keep specificity predictable and source order
explicit - four complementary tools: specificity discipline, cascade layers, naming
conventions, and design tokens.
5.1 Specificity Discipline
The cascade computes a three-part score for every selector:
(ID count, class/attribute/pseudo-class count, element/pseudo-element count).
The selector with the higher score wins, regardless of source order. Conflicts become
hard to reason about when selectors mix IDs, deep nesting, and element qualifiers,
because the winning rule is no longer obvious.
The practical rule: keep every styling selector at exactly one class - specificity
(0,1,0). Deviating upward creates debt that has to be paid by more deviation later.
Table 1. - Selector specificity scores
| Selector |
Score (I, C, E) |
Notes |
* |
(0, 0, 0) |
Universal selector; no contribution. |
p, div, ::before |
(0, 0, 1) |
Element and pseudo-element selectors. |
.card, [type="text"], :hover |
(0, 1, 0) |
Class, attribute, and pseudo-class selectors. |
#nav |
(1, 0, 0) |
ID selector; beats any number of classes. |
:where(...) |
(0, 0, 0) |
Argument list is evaluated but contributes zero specificity. |
:is(...), :not(...) |
Inherited |
Takes the specificity of the most specific selector in its argument list. |
Inline style="" |
(1, 0, 0, 0) |
Above the three-part score; beaten only by !important. |
Three patterns that keep specificity low:
/* instead of: nav > ul > li > a { color: steelblue; } (0,0,3) */
.nav-link { color: steelblue; } /* (0,1,0) */
/* :where() - zero specificity, safe for resets and base styles */
:where(h1, h2, h3) { line-height: 1.2; }
/* :is() - grouping convenience; inherits specificity of its most-specific arg */
:is(.card, .panel) > p { margin-bottom: 0.75rem; }
5.2 Cascade Layers (@layer)
@layer assigns rules to named layers whose win order is declared once,
independently of specificity. A rule in a later layer always beats a same-specificity
rule in an earlier layer. Rules outside any layer sit above all layers and win
unconditionally - useful for keeping third-party imports from overriding your styles.
/* declare order once at the top of the root stylesheet */
@layer base, theme, components, utilities;
@layer base {
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: sans-serif; }
}
@layer theme {
:root {
--color-text: #1a1a2e;
--color-accent: steelblue;
--color-bg: #ffffff;
}
}
@layer components {
.btn { padding: 0.4rem 1rem; border-radius: 4px; }
.card { padding: 1rem; border: 1px solid var(--color-accent); }
}
@layer utilities {
.hidden { display: none; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; }
}
Importing a third-party stylesheet into a layer contains it below your own unlayered
rules:
/* bootstrap stays inside "vendor" - your unlayered rules win */
@layer vendor {
@import url("bootstrap.min.css");
}
Within a layer, normal specificity and source order still apply. Across layers, layer
order decides everything.
5.3 Naming Conventions
A consistent naming convention enforces uniform specificity and makes component
boundaries visible in markup. BEM (Block-Element-Modifier) is the
most widely adopted scheme:
- .block - a standalone component (.card, .nav)
- .block__element - a child part of that component (.card__title)
- .block--modifier - a variant or state (.card--featured, .btn--danger)
/* every selector is a single class: specificity (0,1,0) throughout */
.card { background: var(--color-bg); border-radius: 4px; padding: 1rem; }
.card__title { font-size: 1.1rem; font-weight: bold; margin-bottom: 0.5rem; }
.card__body { color: var(--color-text); }
.card--featured { border: 2px solid var(--color-accent); }
<div class="card card--featured">
<h2 class="card__title">Title</h2>
<p class="card__body">Content</p>
</div>
Because every selector is one class, there are no specificity wars. A modifier class
is added alongside the block class rather than overriding it, so both sets of rules
apply cleanly.
5.4 Design Tokens with Custom Properties
Custom properties separate design decisions (color, spacing, radius) from layout
rules. Define a named palette at :root; switch an entire theme by
overriding a handful of properties in one place. Components reference tokens, never
raw values, so a single change propagates everywhere.
/* token definitions */
:root {
--color-text: #1a1a2e;
--color-bg: #ffffff;
--color-accent: steelblue;
--color-accent-dim: #b0c8e0;
--space-sm: 0.5rem;
--space-md: 1.0rem;
--space-lg: 2.0rem;
--radius: 4px;
--shadow: 0 2px 8px rgb(0 0 0 / 12%);
}
/* dark theme - just override the tokens */
.theme-dark {
--color-text: #e8e8f0;
--color-bg: #1a1a2e;
--color-accent: #7eb8d8;
}
/* component uses only tokens, never raw values */
.card {
background: var(--color-bg);
color: var(--color-text);
border-radius: var(--radius);
padding: var(--space-md);
box-shadow: var(--shadow);
}
Custom properties are inherited like any other CSS property. A token set on a
container applies to all descendants, making scoped overrides straightforward:
/* invert accent inside a hero section without touching component rules */
.hero { --color-accent: #fce8c8; }
5.5 Controlling !important
!important overrides all normal specificity, including inline styles.
That power makes it destructive when used to patch a specificity conflict - the next
developer adds another !important and the arms race begins, eventually
making styles impossible to reason about.
Three legitimate uses:
-
Utility classes that must never be overridden by component styles:
.hidden { display: none !important; }
.visible { display: block !important; }
-
Accessibility overrides - forced contrast, reduced motion:
@media (forced-colors: active) {
.btn { border: 2px solid ButtonText !important; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; }
}
-
Temporary debugging - always removed before committing.
When you find yourself reaching for !important to fix a specificity
conflict, the right fix is almost always to reduce the specificity of the
conflicting rule, restructure the selector, or use @layer to establish
explicit win order.
5.6 Strategy Summary
Table 2. - Style management strategies
| Strategy |
What it controls |
Key rule |
| Specificity discipline |
Which rule wins when two selectors match the same element |
Keep all styling selectors at one class (0,1,0). Use :where() for zero-cost grouping. |
@layer |
Source-order priority across stylesheets and libraries |
Later layers beat earlier layers regardless of specificity. Unlayered rules beat all layers. |
| Naming (BEM) |
Enforces single-class selectors; encodes component structure |
.block__element--modifier - every selector is one class, self-documenting. |
| Design tokens |
Centralizes design values; enables theming without touching component rules |
Define at :root, override in a scoped context. Components reference only tokens. |
!important |
Last-resort override; legitimate only for utilities and accessibility |
Never use to fix a specificity conflict - fix the selector instead. |