CSS Cascade, Specificity & Layers

When several CSS rules target the same element and property, the browser needs a deterministic way to pick one. That's the cascade: a sequence of tie-breakers covering origin and importance, cascade layers, specificity, and finally source order. Most "why isn't my style applying?" moments come from misunderstanding one of these steps, and most unmaintainable stylesheets come from fighting them with ever-longer selectors and !important.

Modern CSS gives you real tools to stay in control: @layer to order entire groups of styles explicitly, :where() for zero-specificity selectors, and @scope for component boundaries. Used together they make third-party CSS, resets, design systems, and utilities coexist predictably.

TL;DR

Quick Example

Ordering a whole stylesheet with cascade layers:

Even though #legacy .vendor-widget .button in the vendor layer has far higher specificity, the components and utilities layers win because layer order is checked before specificity.

Core Concepts

The Cascade Order

For each property on each element, the browser collects all matching declarations and sorts them by:

  1. Origin and importance: user-agent (browser defaults) < user styles < author styles (yours), and for !important declarations the order reverses.
  2. Context: styles inside Shadow DOM vs outside, and @scope proximity.
  3. Cascade layers: explicit @layer order (later wins); unlayered styles win over layered ones for normal declarations.
  4. Specificity: more specific selectors win.
  5. Order of appearance: the last declaration wins.

Inline style="" attributes sit above selectors in specificity terms, and animations and transitions have their own positions in the order.

Specificity

Specificity is a three-part score (A, B, C), compared left to right:

Examples:

One ID outweighs any number of classes, which is why IDs in selectors cause specificity escalation.

Cascade Layers

@layer groups rules into named layers with an explicit priority order, independent of specificity:

Frameworks increasingly ship with layers (Tailwind v4 uses @layer theme, base, components, utilities).

:where(), :is(), and @scope

Inheritance

Some properties inherit from the parent by default (color, font-*, line-height, visibility, cursor); most layout properties don't (margin, padding, border, display, width). Keywords control this explicitly:

Custom properties (--brand) inherit by default, which is what makes them work so well for theming. See CSS custom properties.

CSS Architecture Strategies

Best Practices

Keep Specificity Low and Predictable

Prefer single-class selectors. When you need a descendant rule, wrap the context in :where() so overriding it later doesn't require escalation.

Put Third-Party CSS in a Low Layer

Importing vendor stylesheets into a vendor layer means your components override them without !important or selector hacks.

Use !important Deliberately

Legitimate uses: utility classes meant to always win (.hidden), user accessibility overrides, and protecting critical rules in low layers. If you're adding !important to beat another !important, the architecture needs fixing.

Debug With DevTools

The Styles panel shows every matching rule, crossed-out overridden declarations, the winning layer, and specificity on hover. It's the fastest way to see why a rule lost.

Common Mistakes

Specificity Wars

Flatten selectors, move overrides into higher layers, and use :where() for base styles.

Unlayered Legacy CSS Overriding Layers

After introducing @layer components, old unlayered stylesheets still win over it. Import legacy CSS into a layer (@import url(legacy.css) layer(legacy);) placed where it belongs.

Expecting Margin or Borders to Inherit

.card { border: 1px solid } doesn't give child elements borders. Only inherited properties flow down. Use explicit selectors or custom properties for shared values.

FAQ

How is CSS specificity calculated?

Count IDs (A), then classes, attributes, and pseudo-classes (B), then element types and pseudo-elements (C). Compare A first, then B, then C. The universal selector, combinators, and :where() add nothing, while :is(), :not(), and :has() take the specificity of their most specific argument.

Do cascade layers replace specificity?

No. They sit above specificity in the cascade. Between layers, layer order decides; within the same layer, specificity and then source order still apply. Layers let you stop using specificity as an ordering mechanism between whole categories of styles.

Why does my inline style lose?

Inline styles beat selector-based author styles, except those marked !important. If an inline style loses, an !important declaration, an animation, or user or browser important styles are probably overriding it.

When should I use :where()?

For base styles, resets, and library defaults that consumers should be able to override with any simple selector. Zero specificity makes those styles easy to extend without escalation.

Related Topics

References