What is CSS Specificity? — CSS Selector Priority Explained

What is CSS Specificity? — CSS Selector Priority Explained

Definition

CSS specificity is the algorithm browsers use to determine which CSS property values are most relevant to an element and therefore take precedence when multiple conflicting style declarations target the same element. It is a weight system that assigns a numerical score to each CSS selector based on its composition. The selector with the highest specificity wins when the cascade resolves conflicting rules.

Think of specificity as a tiebreaker in the CSS cascade: when two rules have the same origin and importance, the one with the higher specificity is applied.


The Specificity Hierarchy

CSS specificity is calculated as a four-part value (a, b, c, d) — often represented as a three-part tuple (a, b, c) or calculated as a single number. Each position in the hierarchy carries exponentially more weight than the next:

Level 1: Inline Styles (a)

Inline styles applied directly via the style attribute have the highest specificity. They override any selector-based rule unless !important is used.

Position Type Weight Value
a Inline styles (style attribute) 1,0,0,0

Level 2: ID Selectors (b)

Each ID selector (#id) counts toward the second weight column.

Position Type Weight Value
b ID selectors (#example) 0,1,0,0

Level 3: Class, Attribute, and Pseudo-Class Selectors (c)

Each class selector (.class), attribute selector ([type="text"]), and pseudo-class (:hover, :nth-child()) counts toward this column.

Position Type Weight Value
c Classes, attributes, pseudo-classes 0,0,1,0

Level 4: Element and Pseudo-Element Selectors (d)

Each element selector (div, p) and pseudo-element (::before, ::after) counts toward this column.

Position Type Weight Value
d Elements, pseudo-elements 0,0,0,1

How Specificity Is Calculated

The specificity score is computed by counting the number of selectors at each level. The result is a composite value — a four-part comma-separated number like (0, 1, 2, 3).

Calculation Formula

Specificity = (count of inline styles, count of ID selectors, count of class/attribute/pseudo-class selectors, count of element/pseudo-element selectors)

When comparing two selectors, the browser compares left to right. If a differs, the larger a wins. If equal, compare b, and so on.

Important: values at each level are not summed across levels. 0,1,0,0 always beats 0,0,100,0 — one ID always beats any number of classes.


Real Selector Weight Examples

Selector Inline IDs Classes/Attributes/Pseudo Elements Total Specificity
style="color: red" 1 0 0 0 1,0,0,0
#sidebar 0 1 0 0 0,1,0,0
.menu-item 0 0 1 0 0,0,1,0
div 0 0 0 1 0,0,0,1
#header .nav a 0 1 1 1 0,1,1,1
ul li a.active 0 0 1 3 0,0,1,3
div#content article p 0 1 0 3 0,1,0,3
a:hover 0 0 1 1 0,0,1,1
input[type="text"] 0 0 1 1 0,0,1,1
#main .card:nth-child(2) 0 1 2 0 0,1,2,0
`` 0 0 0 0 0,0,0,0

Detailed Walkthrough: #header .nav a

#header     → 1 ID selector       → 0,1,0,0
.nav        → 1 class selector    → 0,0,1,0
a           → 1 element selector  → 0,0,0,1
─────────────────────────────────────────────
Total       →                      → 0,1,1,1

Detailed Walkthrough: div#content article p

div         → 1 element selector  → 0,0,0,1
#content    → 1 ID selector       → 0,1,0,0
article     → 1 element selector  → 0,0,0,1
p           → 1 element selector  → 0,0,0,1
─────────────────────────────────────────────
Total       →                      → 0,1,0,3

The !important Exception

The !important flag is a special declaration that overrides all specificity rules:

.overridden {
  color: blue;
}
 
.override-all {
  color: red !important; /* This wins regardless of specificity */
}

How !important Changes the Cascade

When !important is used, the cascade order becomes:

  1. Normal declarations sorted by origin
  2. Normal declarations sorted by specificity
  3. !important declarations sorted by origin
  4. !important declarations sorted by specificity

Rules for !important

  • !important declarations override all non-important declarations, even inline styles
  • If two conflicting rules both have !important, normal specificity rules apply
  • If two !important rules have equal specificity, the one declared later wins

When !important Is Acceptable

  • Utility classes (e.g., .hidden { display: none !important; })
  • Overriding third-party styles when you cannot modify the source
  • Print stylesheets that must override screen styles
  • Accessibility overrides (e.g., forced visibility)

In most cases, avoid !important — it breaks the natural cascade and makes debugging significantly harder.


The Universal Selector and Combinators

Selector / Pattern Specificity Contribution Notes
* (universal) 0,0,0,0 Adds no specificity
> (child) 0,0,0,0 Combinator adds nothing
+ (adjacent) 0,0,0,0 Combinator adds nothing
~ (sibling) 0,0,0,0 Combinator adds nothing
`` (descendant) 0,0,0,0 Whitespace adds nothing
:not() Counts the argument's specificity The :not() itself adds nothing
:is() Takes the highest specificity of its arguments Can cause unexpected weight
:where() 0,0,0,0 Always zero specificity

Special Pseudo-Classes

  • :not(selector) — the negation pseudo-class itself adds no specificity, but the selector inside the parentheses is counted normally.
  • :is(selector1, selector2) — takes the specificity of the most specific argument.
  • :where(selector1, selector2) — always contributes 0,0,0,0 regardless of its arguments. Useful for low-specificity overrides.

Common Specificity Issues

1. Overspecified Selectors

Problem: Writing div#content div.container div.row div.col p when .col p would suffice.

Solution: Keep selectors as flat and short as possible. Deeply nested selectors are brittle and harder to override.

2. ID Selectors in Global CSS

Problem: #sidebar .widget-title requires an ID selector to override, forcing future developers to use !important or even more IDs.

Solution: Prefer class selectors over IDs for styling. Reserve IDs for JavaScript hooks or fragment identifiers.

3. Specificity Wars

Problem: Developer A writes .nav-item a, Developer B writes .header .nav-item a to override it, Developer C adds #page .header .nav-item a. Each override increases specificity unnecessarily.

Solution: Agree on a consistent specificity level across the project. Use BEM or similar methodologies to keep specificity flat and predictable.

4. Overusing !important

Problem: A quick !important fix today becomes a debugging nightmare tomorrow when multiple !important declarations conflict.

Solution: Treat !important as a last resort. Refactor the source selector instead of brute-forcing an override.

5. :is() Surprises

/* This has specificity 0,1,0,0 because :is() takes the highest argument */
:is(#sidebar, .widget) a {
  color: blue;
}
 
/* This has specificity 0,0,1,0 */
.widget a {
  color: red;
}

The first rule wins even though it looks like it should be overridden by the more specific-seeming second rule.


Specificity Comparison Table

Selector Pattern Inline IDs Classes/Attrs Elements Specificity
* { } 0 0 0 0 0,0,0,0
p { } 0 0 0 1 0,0,0,1
.class { } 0 0 1 0 0,0,1,0
p.class { } 0 0 1 1 0,0,1,1
#id { } 0 1 0 0 0,1,0,0
#id .class { } 0 1 1 0 0,1,1,0
#id .class p { } 0 1 1 1 0,1,1,1
p.class#id { } 0 1 1 1 0,1,1,1
style="color:red" 1 0 0 0 1,0,0,0
style + !important 1 0 0 0 Wins everything
p:first-child { } 0 0 1 1 0,0,1,1
ul > li { } 0 0 0 2 0,0,0,2
a[href*="example"] 0 0 1 1 0,0,1,1
div:nth-child(2n+1) 0 0 1 1 0,0,1,1
:not(.hidden) { } 0 0 1 0 0,0,1,0
:is(#id, .cls) { } 0 1 0 0 0,1,0,0

CSS Cascade Order Recap

The specificity algorithm is one step in the broader CSS cascade, which follows this resolution order:

  1. Origin and importance — browser styles < user styles < author styles < author !important < user !important < browser !important
  2. Specificity — higher specificity wins
  3. Source order — later declarations override earlier ones when origin and specificity are equal

Best Practices

1. Use BEM (Block Element Modifier)

BEM naming keeps specificity predictably flat — every selector is exactly one class:

/* BEM: all single classes, specificity always 0,0,1,0 */
.card { }
.card__title { }
.card__title--featured { }
.card__button { }

2. Keep Selectors Flat

Avoid nesting beyond 3 levels. Prefer:

.nav-link { }           /* good */
.nav-link--active { }   /* good */

Over:

header nav ul li a.active { }  /* overly specific and fragile */

3. Avoid ID Selectors for Styling

Use IDs for JavaScript or fragment identifiers. Use classes for CSS. This prevents accidentally creating selectors that are nearly impossible to override.

4. Never Use !important in Application CSS

Reserve !important for utilities and third-party overrides. If you need !important to make your CSS work, your specificity structure needs refactoring.

5. Leverage :where() for Low-Specificity Overrides

/* Both target the same elements, but :where() adds zero specificity */
:where(.card) .title { color: blue; }       /* specificity: 0,0,0,1 */
.card .title { color: red; }                /* specificity: 0,0,2,0 — wins */

6. Test Specificity with Tools

Use a CSS Specificity Calculator to see exact weights before writing rules. This prevents surprises during development.

7. Follow a CSS Methodology

  • BEM — Best for component-based architectures
  • SMACSS — Categorizes rules into base, layout, module, state, theme
  • ITCSS — Layers CSS from lowest to highest specificity
  • CSS Modules — Automatically scoped class names

Specificity in Practice: Before and After

The Problem

/* A mess of escalating specificity */
#content .sidebar .widget .title a {
  color: blue; /* 0,1,3,1 */
}

The Solution

/* Clean, flat, maintainable */
.widget-link {
  color: blue; /* 0,0,1,0 */
}
 
/* Override just as easily */
.dark-theme .widget-link {
  color: white; /* 0,0,2,0 */
}

Quick Reference: Specificity Cheatsheet

Want to style... Use Specificity
A specific element a 0,0,0,1
A group of elements .btn 0,0,1,0
A unique element #header (avoid if possible) 0,1,0,0
An element inline style="" 1,0,0,0
Everything * 0,0,0,0
Skip specificity !important Breaks rules

LangStop CSS Tools