CSSintermediate

CSS Specificity & Cascade

Master CSS specificity calculation, selector weights, the cascade algorithm, inheritance, and strategies to avoid specificity wars in your stylesheets.

13 min readยทPublished Apr 1, 2026
cssspecificitycascadeselectors

Why Specificity Matters

When multiple CSS rules target the same element, the browser needs to decide which rule wins. This decision is made by the cascade โ€” a set of rules that determines which styles apply. Specificity is the primary tiebreaker in the cascade.

Understanding specificity prevents the most common CSS frustration: "Why isn't my style being applied?"

/* Which color wins? */
p { color: blue; }
.text { color: green; }
#intro { color: red; }

The answer depends on specificity. If an element is <p class="text" id="intro">, all three rules match. The #intro rule wins because ID selectors have the highest specificity among these three.

The Cascade Algorithm

The cascade resolves style conflicts in this order:

1. Origin & Importance
   โ†’ User agent (browser defaults)
   โ†’ Author (your CSS)
   โ†’ Author !important
   โ†’ User !important
   โ†’ Animations (@keyframes)
   โ†’ Transitions

2. Specificity (if same origin)
   โ†’ Compare specificity scores

3. Source Order (if same specificity)
   โ†’ Last rule in source order wins

Most conflicts are resolved at step 2 (specificity) or step 3 (source order). Step 1 only matters when !important is involved or when overriding browser defaults.

Layer Order (@layer)

CSS Cascade Layers, introduced more recently, add another dimension:

@layer base, components, utilities;

@layer base {
  p { color: blue; }
}

@layer utilities {
  .text-red { color: red; }  /* wins over base, regardless of specificity */
}

Layers are resolved before specificity. Styles in later layers win over earlier layers, even if the earlier layer has higher specificity selectors. Unlayered styles win over all layers.

Specificity Calculation

Specificity is expressed as a three-part score: (A, B, C)

  • A = number of ID selectors
  • B = number of class selectors, attribute selectors, and pseudo-classes
  • C = number of type selectors and pseudo-elements
Selector                    | A | B | C | Specificity
----------------------------|---|---|---|------------
p                           | 0 | 0 | 1 | (0,0,1)
.widget                     | 0 | 1 | 0 | (0,1,0)
p.widget                    | 0 | 1 | 1 | (0,1,1)
#sidebar                    | 1 | 0 | 0 | (1,0,0)
#sidebar .widget             | 1 | 1 | 0 | (1,1,0)
#sidebar .widget p           | 1 | 1 | 1 | (1,1,1)
#sidebar #main .widget p     | 2 | 1 | 1 | (2,1,1)

Higher specificity wins. Comparison is done left to right:

  • (1,0,0) beats (0,99,99) โ€” one ID beats any number of classes.
  • (0,2,0) beats (0,1,5) โ€” two classes beat one class + five types.
  • (0,0,3) beats (0,0,2) โ€” three types beat two types.

Selector Types and Their Weight

A-level (ID selectors): Specificity (1,0,0)

#header { }           /* (1,0,0) */
#header #nav { }      /* (2,0,0) */

B-level (Class, attribute, pseudo-class): Specificity (0,1,0) each

.button { }           /* (0,1,0) */
.button.primary { }   /* (0,2,0) */
[type="text"] { }     /* (0,1,0) */
:hover { }            /* (0,1,0) */
:first-child { }      /* (0,1,0) */
:not(.hidden) { }     /* (0,1,0) โ€” :not itself has zero specificity */
                       /* but .hidden inside adds (0,1,0) */

C-level (Type selectors, pseudo-elements): Specificity (0,0,1) each

div { }               /* (0,0,1) */
div p { }             /* (0,0,2) */
::before { }          /* (0,0,1) */
div::after { }        /* (0,0,2) */

Zero specificity

* { }                 /* (0,0,0) โ€” universal selector */
:where(.class) { }   /* (0,0,0) โ€” :where always zero */

Special Cases

:is() and :not() take the specificity of their most specific argument:

:is(.a, #b, p) { }
/* Specificity = (1,0,0) because #b is the most specific argument */

:not(#main) { }
/* Specificity = (1,0,0) */

:not(.a, .b) { }
/* Specificity = (0,1,0) because .a and .b are equal */

:where() always has zero specificity, regardless of what's inside:

:where(#main .widget p) { }
/* Specificity = (0,0,0) โ€” always zero */

This makes :where() useful for writing overridable base styles.

:has() takes the specificity of its argument:

:has(> .active) { }
/* Specificity = (0,1,0) from .active */

Inline Styles

Inline styles (the style attribute) override all selector-based specificity. Think of them as (1,0,0,0) โ€” a fourth level above IDs.

<p style="color: red;" class="blue" id="intro">
  <!-- color is red โ€” inline styles win over all selectors -->
</p>
#intro { color: blue; }     /* (1,0,0) โ€” loses to inline */
p.blue { color: blue; }     /* (0,1,1) โ€” loses to inline */

Only !important can override inline styles.

!important

!important overrides all normal specificity. It moves the rule to a higher origin level in the cascade.

p { color: blue !important; }
#intro { color: red; }

/* blue wins โ€” !important overrides normal specificity */

!important vs !important

When multiple !important declarations compete, specificity decides among them:

p { color: blue !important; }         /* (0,0,1) with !important */
.text { color: green !important; }    /* (0,1,0) with !important */
#intro { color: red !important; }     /* (1,0,0) with !important */

/* red wins โ€” highest specificity among !important rules */

When to Use !important

Almost never. Legitimate use cases:

/* 1. Overriding third-party CSS you can't modify */
.third-party-widget p {
  color: inherit !important;
}

/* 2. Utility classes that must always apply */
.hidden { display: none !important; }
.sr-only { /* screen reader only */
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
}

/* 3. Print styles */
@media print {
  .no-print { display: none !important; }
}

If you're using !important to fix specificity conflicts in your own code, it means your selector architecture needs restructuring.

Inheritance

Some CSS properties are inherited by default โ€” children automatically receive the parent's value. Others are not inherited.

Properties That Inherit

/* Typography */
color, font-family, font-size, font-weight, font-style,
line-height, letter-spacing, word-spacing, text-align,
text-indent, text-transform, white-space

/* Lists */
list-style, list-style-type, list-style-position

/* Other */
visibility, cursor, direction

Properties That Do NOT Inherit

/* Box model */
margin, padding, border, width, height,
min-width, max-width, min-height, max-height

/* Layout */
display, position, float, clear, overflow

/* Background */
background, background-color, background-image

/* Other */
box-shadow, opacity, transform, z-index

Controlling Inheritance

.child {
  color: inherit;   /* explicitly inherit from parent */
  border: inherit;  /* inherit a normally non-inherited property */
  margin: initial;  /* reset to browser default */
  padding: unset;   /* inherit if normally inherited, initial if not */
  all: unset;       /* reset ALL properties */
}
/* Useful pattern: reset element to inherit everything */
.inherit-all {
  all: inherit;
}

/* Reset element to browser defaults */
.reset-all {
  all: initial;
}

/* Unset: acts as inherit for inherited properties, initial for others */
.clean-slate {
  all: unset;
}

Cascade Order (Source Order)

When two rules have the same specificity and same origin, the last one in source order wins.

.button { color: blue; }
.button { color: red; }

/* red wins โ€” same specificity, later in source */

This applies across files too. If you load base.css before theme.css, rules in theme.css win (assuming same specificity).

<link rel="stylesheet" href="base.css">    <!-- loads first -->
<link rel="stylesheet" href="theme.css">   <!-- loads second โ€” wins -->

Source Order in Practice

/* base.css */
.btn { background: gray; }

/* theme.css (loaded after base.css) */
.btn { background: blue; }  /* wins โ€” same specificity, later source */

Specificity Examples

Example 1: Class vs Element

h1 { color: blue; }        /* (0,0,1) */
.title { color: red; }     /* (0,1,0) */
<h1 class="title">Hello</h1>
<!-- Color: red โ€” class (0,1,0) beats element (0,0,1) -->

Example 2: Multiple Classes vs ID

.nav .list .item .link { color: blue; }  /* (0,4,0) */
#nav-link { color: red; }                /* (1,0,0) */
<a class="link" id="nav-link">Click</a>
<!-- Color: red โ€” one ID (1,0,0) beats four classes (0,4,0) -->

Example 3: Inline vs ID + !important

<p id="intro" style="color: blue;">Text</p>
#intro { color: red !important; }
/* Color: red โ€” !important overrides inline styles */

Example 4: Nested Selectors

.sidebar .widget { color: blue; }    /* (0,2,0) */
.widget { color: red; }              /* (0,1,0) */
<div class="sidebar">
  <div class="widget">Text</div>
</div>
<!-- Color: blue โ€” (0,2,0) beats (0,1,0) -->

Example 5: :where() for Low Specificity

:where(.sidebar .widget) { color: blue; }  /* (0,0,0) */
.widget { color: red; }                     /* (0,1,0) */
<div class="sidebar">
  <div class="widget">Text</div>
</div>
<!-- Color: red โ€” :where() has zero specificity -->

Strategies to Avoid Specificity Wars

Strategy 1: Keep Selectors Flat

/* BAD โ€” high specificity, hard to override */
#sidebar .nav .list .item a.active { color: red; }

/* GOOD โ€” flat, low specificity, easy to override */
.nav-link-active { color: red; }

Strategy 2: Use BEM Naming Convention

BEM (Block Element Modifier) uses single-class selectors, keeping specificity uniformly low.

/* Block */
.card { }

/* Element (part of a block) */
.card__title { }
.card__body { }
.card__footer { }

/* Modifier (variation) */
.card--featured { }
.card__title--large { }

Every selector is a single class: specificity (0,1,0). No nesting, no IDs.

Strategy 3: Avoid IDs for Styling

IDs are for JavaScript and anchor links. Use classes for styling.

/* BAD โ€” (1,0,0) specificity, hard to override */
#header { background: white; }

/* GOOD โ€” (0,1,0) specificity, easy to override */
.header { background: white; }

Strategy 4: Use :where() for Base Styles

/* Base styles โ€” zero specificity, easily overridable */
:where(.btn) {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  background: gray;
}

/* Variant โ€” (0,1,0), wins over base */
.btn-primary {
  background: blue;
}

Strategy 5: Use Cascade Layers

@layer base, components, utilities;

@layer base {
  a { color: blue; text-decoration: underline; }
}

@layer components {
  .nav-link { color: inherit; text-decoration: none; }
}

@layer utilities {
  .text-red { color: red; }
}

/* Layers resolve before specificity:
   utilities > components > base */

Strategy 6: Single Source of Truth Per Element

Each element should have at most one or two selectors that style it. If you need many selectors for one element, the architecture needs rethinking.

/* BAD โ€” multiple sources fighting */
.sidebar a { color: blue; }
.nav a { color: green; }
.sidebar .nav a { color: red; }  /* need this to fix the conflict */

/* GOOD โ€” one selector per concern */
.nav-link { color: green; }
.nav-link--sidebar { color: blue; }

Debugging Specificity in DevTools

Chrome DevTools

  1. Inspect an element and look at the Styles panel.
  2. Rules are listed in specificity order (highest first).
  3. Strikethrough indicates a property was overridden by a more specific rule.
  4. Computed tab shows the final applied values with a link to which rule won.
  5. Filter by property name to see all competing rules for that property.
Styles panel view:

element.style {              <- inline styles (highest)
  color: red;
}

#intro {                     <- ID selector
  color: blue;               <- strikethrough (overridden by inline)
}

.text {                      <- class selector
  color: green;              <- strikethrough
  font-size: 1rem;           <- active (no competition)
}

Tips

  • Hover over a selector in DevTools to see its specificity score.
  • Use the "Computed" tab to trace which rule ultimately provides each property.
  • Chrome shows the cascade order โ€” look for strikethroughs to identify losing rules.

Specificity Quick Reference

Selector TypeExampleABC
Universal*000
Typediv001
Pseudo-element::before001
Class.card010
Attribute[href]010
Pseudo-class:hover010
ID#main100
Inline stylestyle=""---
!important!importantOverrides all
:where():where(#x)000
:is():is(#x, .y)100
:not():not(.x)010
:has():has(.x)010

Cascade Resolution Order

1. Relevance     โ€” Does the rule match the element?
2. Origin        โ€” Where does the rule come from?
                    (user-agent < author < author !important)
3. Layer         โ€” Which @layer is the rule in?
                    (earlier layers < later layers < unlayered)
4. Specificity   โ€” (A,B,C) comparison
5. Source Order  โ€” Last rule in the stylesheet wins

Common Pitfalls

Pitfall 1: Overusing Descendant Selectors

/* BAD: specificity creep */
.page .content .article .paragraph a { color: blue; }
/* (0,4,1) โ€” now you need even more specificity to override */

/* GOOD: flat selector */
.article-link { color: blue; }
/* (0,1,0) โ€” easy to override or extend */

Pitfall 2: !important Chains

/* This is a specificity war in progress */
.btn { color: white !important; }
.btn-danger { color: red !important; }
.btn-danger.btn-lg { color: darkred !important; }
/* Each rule needs !important to win over the previous one */

Pitfall 3: Forgetting About :is() Specificity

:is(.card, #sidebar) .title { color: blue; }
/* Specificity = (1,1,0) because #sidebar is the highest inside :is() */
/* Even .card .title gets the ID-level specificity */

Pitfall 4: Third-Party CSS Conflicts

Third-party CSS can introduce high-specificity selectors. Solutions:

  1. Load your CSS after third-party CSS.
  2. Use more specific selectors (reluctantly).
  3. Use @layer to control cascade order.
  4. Use !important as a last resort.
/* Contain third-party styles in a layer */
@layer third-party {
  @import url('third-party.css');
}

/* Your styles are unlayered โ€” always win */
.your-class { color: red; }

Key Takeaways

  • Specificity is calculated as (A, B, C): IDs, classes/pseudo-classes/attributes, types/pseudo-elements.
  • One ID beats any number of classes. One class beats any number of type selectors.
  • !important overrides all normal specificity. Use it only as a last resort.
  • Inline styles beat all selector-based rules (unless !important is used).
  • When specificity is equal, source order wins โ€” last rule applied takes effect.
  • :where() always has zero specificity โ€” great for base/default styles.
  • :is() and :not() take the specificity of their most specific argument.
  • Keep selectors flat and low-specificity. Prefer single classes.
  • Avoid styling with IDs โ€” use classes instead.
  • Use @layer to manage cascade order across large codebases.
  • If you're reaching for !important, your architecture probably needs restructuring.
  • DevTools Styles panel shows you exactly which rules win and why.

Found this helpful?

Support devsofus โ€” help us keep creating free dev guides.

Related Articles