Every time a browser renders a page, it resolves thousands of potential conflicts: multiple rules targeting the same element, declarations scattered across author stylesheets and browser defaults, inline styles competing with external files. The mechanism that settles all of these disputes is the CSS cascade — and understanding it precisely is the difference between CSS that you control and CSS that controls you. Long before native cascade layers existed, teams managed specificity primarily through authoring discipline enforced by CSS preprocessors like Sass and Less — nesting conventions, naming methodologies, and mixin patterns that tried to keep specificity predictable without any native browser mechanism to enforce it.

The cascade is defined in the CSS Cascading and Inheritance specification, currently at Level 5. It operates as a sorting algorithm applied to every CSS declaration. When two or more declarations target the same property on the same element, the cascade determines which one wins by evaluating four criteria in sequence: origin and importance, specificity, and order of appearance. Inheritance is a related but separate mechanism — it kicks in only when no cascade winner exists.

Origin and Importance

Before specificity enters the picture, the cascade considers where a declaration comes from and whether it carries the !important flag.

CSS declarations can originate from three sources:

  • User-agent stylesheets — the browser’s built-in defaults (<h1> gets font-size: 2em, <a> gets color: blue, and so on)
  • Author stylesheets — everything your application delivers, including external stylesheets, <style> blocks, and inline style attributes
  • User stylesheets — custom stylesheets applied by the user through browser extensions or accessibility tools

The cascade applies a priority order across these origins. For normal (non-!important) declarations, author styles beat user styles, which beat user-agent styles. The intuition is that web developers know their design intent better than browser defaults.

!important reverses this order within each origin tier. A !important user-agent declaration outranks a normal author declaration. More practically relevant: a !important user declaration outranks a !important author declaration — which is intentional. Users who configure high-contrast modes or larger font sizes via browser extensions should be able to override author styles. The spec respects that hierarchy.

The CSS Cascade Level 5 spec introduced two additional origin layers — author presentational hints and cascade layers (the @layer at-rule) — which sit between the existing tiers and give authors finer-grained control over ordering without reaching for !important.

Cascade Layers

@layer landed in all major browsers in 2022. It lets you declare named layers and control their priority explicitly:

@layer reset, base, components, utilities;

@layer reset {
  * { box-sizing: border-box; margin: 0; }
}

@layer components {
  .card { background: white; border-radius: 4px; }
}

@layer utilities {
  .mt-4 { margin-top: 1rem; }
}

Unlayered styles (authored outside any @layer block) win over layered styles, regardless of specificity. Within layers, the last declared layer wins when specificities are equal. This is significant: @layer lets you build low-specificity utility classes that override component styles without writing high-specificity selectors or resorting to !important.

Specificity: The Four-Column Score

When two declarations share the same origin and importance tier, the cascade moves to specificity — a weight assigned to each selector based on its components.

The Selectors Level 4 spec represents specificity as three columns, often written as (A, B, C):

ColumnWhat counts
AID selectors (#sidebar)
BClass selectors (.card), attribute selectors ([type="text"]), pseudo-classes (:hover, :focus)
CType selectors (p, div, li), pseudo-elements (::before, ::after)

The universal selector (*), combinators (>, +, ~, ), and the :where() pseudo-class contribute zero specificity.

Specificity columns are compared left to right. A single ID (1,0,0) beats any number of class selectors. Ten class selectors (0,10,0) still lose to one ID (1,0,0) — there is no carrying between columns. Inline styles sit above the selector-based columns entirely; they carry an implicit (1,0,0,0) weight that no selector can match without !important.

Worked Examples

/* specificity: 0,0,1 — one type selector */
p { color: black; }

/* specificity: 0,1,0 — one class selector */
.intro { color: navy; }

/* specificity: 0,1,1 — one class + one type */
p.intro { color: navy; }

/* specificity: 1,0,0 — one ID selector */
#hero { color: crimson; }

/* specificity: 1,1,1 — ID + class + type */
#hero p.intro { color: darkblue; }

For an element <p id="hero" class="intro">, the declaration from #hero p.intro wins because 1,1,1 is the highest specificity in that list.

This also explains why CSS written without a principled architecture tends to escalate. One developer adds .sidebar .nav li a (0,2,2) to override .nav a (0,1,1). Another adds #sidebar .nav li a (1,2,2) to override that. Eventually someone reaches for !important — and the cascade becomes a war of overrides rather than a composable system.

The Specificity Footprint of :is(), :not(), and :has()

Three pseudo-classes deserve special attention because they do not behave like others.

:is() takes the specificity of its most specific argument, not the pseudo-class itself. So :is(#hero, .card, p) carries specificity 1,0,0 — the weight of #hero — even when it matches an element via the p selector. This is a deliberate design choice that makes :is() useful for grouping selectors without losing predictability, but it can produce surprises when the argument list includes IDs.

:not() follows the same rule as of Selectors Level 4. :not(.disabled) contributes 0,1,0. Earlier implementations (Selectors Level 3) treated :not() as zero-specificity, which is now obsolete.

:has() — the relational pseudo-class that finally shipped broadly in 2023 — also takes the specificity of its argument. a:has(> img) carries specificity 0,0,2 (one type for a, one type for img). nav:has(.active) carries 0,1,1.

For deeper reading on how selectors compose in modern CSS, the selectors guide on MDN Web Docs covers the full set with live examples.

Order of Appearance

When origin and specificity are equal, the cascade uses order: the declaration that appears later in the source wins. This applies within a single stylesheet and across multiple stylesheets linked in order.

/* Both rules have specificity 0,1,0 */
.button { background: steelblue; }
.button { background: tomato; }
/* tomato wins — it appears later */

This is why stylesheet link order matters. A reset or normalize stylesheet should always be linked before component styles, which should be linked before utility overrides. Relying on source order as a tiebreaker is fine when the system is intentional — it becomes a problem when it is the only mechanism keeping styles from colliding.

Inheritance vs. the Cascade

Inheritance is often conflated with the cascade, but it operates differently. The cascade determines which declaration wins for a given element. Inheritance applies when no cascade winner exists for a property on an element — in that case, the element checks whether the property is inheritable and, if so, takes the computed value from its parent.

Properties like color, font-family, line-height, and font-size inherit by default. Properties like margin, padding, border, and background do not. The full list is defined in each property’s specification.

Four universal keyword values let you manipulate inheritance explicitly:

  • inherit — force inheritance from the parent, even for non-inheritable properties
  • initial — reset to the property’s initial value as defined in the spec
  • unset — behave like inherit for inheritable properties, initial for non-inheritable ones
  • revert — roll back to the browser’s user-agent stylesheet value

revert is particularly useful in component architectures where you want a scoped reset without touching the cascade globally. See also the discussion of CSS resets and normalization for how these values interact with baseline stylesheets.

When !important Is Legitimate

!important has a reputation as a code smell, and often it is. Used reactively — to override a selector you do not control or do not want to refactor — it contributes to specificity arms races and makes the cascade harder to reason about.

But there are legitimate uses:

Accessibility overrides. A user stylesheet that enforces minimum font sizes or high-contrast colors must be able to override author styles. !important in user stylesheets is the specified mechanism for this. When you write !important in an author stylesheet without a good reason, you risk blocking those overrides.

Animation keyframes. Within @keyframes, !important declarations are treated as not having !important — the spec explicitly ignores it. This is by design: animations should not be able to permanently override non-animated values.

Utility classes in low-layer architectures. If your utility layer is not using @layer, !important on utilities (.hidden { display: none !important; }) is a deliberate choice to ensure utilities always apply. This is the approach Tailwind CSS historically used. With @layer now widely supported, this use case is largely superseded.

The test: if !important is solving a conflict that could be resolved by restructuring specificity or layer order, it is a symptom of an architecture problem, not a solution.

Practical Strategies for Low-Specificity Architecture

The goal of a maintainable CSS codebase is a flat specificity graph — declarations clustered around 0,1,0 or 0,0,1, with very few outliers. This makes overriding predictable: you always know roughly what specificity you need to win.

BEM (Block Element Modifier) achieves this by restricting selectors to single classes. .card__title--large is 0,1,0. There are no descendant combinators, no type selectors, no IDs in component styles. The tradeoff is verbose HTML class attributes; the benefit is complete specificity predictability.

Custom properties for theming. CSS custom properties (variables) are not subject to the cascade in the same way as regular declarations — they are resolved at computed-value time. This makes them powerful for theming: a component defined with color: var(--color-text) can be themed at any ancestor level without touching the component’s own specificity.

:root {
  --color-text: #1a1a1a;
  --color-accent: #0057b8;
}

.theme-dark {
  --color-text: #f0f0f0;
  --color-accent: #66aaff;
}

No !important. No specificity escalation. The component remains unchanged; the context determines the output.

@layer for third-party integration. When pulling in a third-party CSS library, wrap it in a named layer:

@layer vendor {
  @import url("third-party.css");
}

@layer components {
  /* your styles, which now outrank vendor by default */
}

This eliminates the need to audit every selector in the library for specificity conflicts. Your unlayered or higher-layer styles win by design.

Avoiding ID selectors in stylesheets. IDs are legitimate in HTML for fragment links and JavaScript hooks, but using them as CSS selectors creates a specificity cliff that is hard to climb without more IDs or !important. If you need to scope styles to a unique element, a single class is sufficient and stays within the same specificity column as everything else.

For a broader look at how selector strategy connects to component-based design systems, the principles here apply at scale: specificity architecture is a design decision, not an afterthought.

The Cascade as a Feature, Not a Bug

The cascade is frequently experienced as a source of frustration — styles that “randomly” override each other, specificity that is hard to calculate at a glance, !important declarations that cannot be undone without more !important. Most of that frustration traces back to an incomplete mental model of how the algorithm works.

Once you internalize the sequence — origin and importance first, then specificity, then order — the cascade becomes a tool you can reason about. Specificity is arithmetic; the columns are predictable. @layer gives you explicit control over the origin tier. Custom properties let you decouple theming from the cascade entirely.

The browser’s job is to apply exactly one value per property per element. Every declaration you write is a candidate. The cascade is the referee — and knowing its rulebook is the foundation of every other CSS skill.