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
- The cascade resolves conflicts by: origin and importance → context/scope → cascade layers → specificity → source order.
- Specificity is counted as (IDs, classes/attributes/pseudo-classes, elements/pseudo-elements). Higher wins regardless of order.
@layerorders groups of rules: later layers win over earlier ones, and unlayered styles beat all layers.:where()adds zero specificity;:is()and:not()take the specificity of their most specific argument.!importantflips priority and reverses layer order. Reserve it for utilities and overrides you truly intend.- Some properties inherit (color, font); control them with
inherit,initial,unset, andrevert.
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:
- Origin and importance: user-agent (browser defaults) < user styles < author styles (yours), and for
!importantdeclarations the order reverses. - Context: styles inside Shadow DOM vs outside, and
@scopeproximity. - Cascade layers: explicit
@layerorder (later wins); unlayered styles win over layered ones for normal declarations. - Specificity: more specific selectors win.
- 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:
- Declare the order up front:
@layer reset, base, components, utilities;. - Import third-party CSS into a low layer:
@import url(lib.css) layer(vendor);. - Nested layers (
@layer components.buttons) organize large systems. - Unlayered styles win over all layers. That's handy for quick overrides, but it means legacy unlayered CSS beats your layered design system until you migrate it into a layer.
- For
!important, layer priority reverses: an important declaration inresetbeats one inutilities, which lets low layers protect critical rules.
Frameworks increasingly ship with layers (Tailwind v4 uses @layer theme, base, components, utilities).
:where(), :is(), and @scope
:where(...)matches like:is()but contributes zero specificity, which is perfect for defaults that should be trivially overridable (resets, library base styles).:is(...)shortens selector lists, taking the highest specificity among its arguments.@scope (.card) to (.card-content) { … }limits styles to a subtree, with an optional lower boundary, and prefers the nearest scope root when rules conflict. It provides component isolation without naming conventions or build tools, and it's supported in modern browsers (check your support matrix).
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
- Low, flat specificity: style with single classes (BEM, utility classes), and avoid IDs and deep descendant chains.
- Layers for ordering: reset → vendor → base → components → utilities.
- Scoping: CSS Modules, CSS-in-JS, Shadow DOM, or
@scopeprevent accidental collisions between components. - Tokens in custom properties keep values consistent across the system. See design tokens.
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
- CSS — The styling language overview
- CSS Custom Properties — Inheritable variables for theming
- CSS-in-JS — Component-scoped styling approaches
- Tailwind — Utility-first CSS built on layers
- Design Systems — Scalable CSS architecture
- Design Tokens — Shared values across a system