Skip to main content

CSS selector specificity

CSS selector specificity is a scoring system browsers use to decide which CSS rule wins when multiple rules target the same element.

Theory

TL;DR

  • Think of it like a bidding system: inline styles bid $1000, each ID bids $100, each class bids $10, each tag bids $1. Highest total wins.
  • Scores are written as four numbers (A,B,C,D): inline styles / IDs / classes+attributes+pseudo-classes / elements+pseudo-elements.
  • If scores tie, the rule declared last in the stylesheet wins.
  • !important bypasses specificity entirely and creates its own override layer.
  • Prefer classes over IDs for most rules. They are much easier to override later.

Quick example

css
div { color: blue; } /* 0,0,0,1 - element */ .highlight { color: red; } /* 0,0,1,0 - class, beats element */ #main .highlight { color: green; } /* 0,1,1,0 - ID+class, beats class */
html
<div id="main" class="highlight">What color is this?</div>

The text is green. #main .highlight scores (0,1,1,0), which beats both (0,0,1,0) and (0,0,0,1). Browsers compare columns left to right. The first column where the numbers differ decides the winner.

How the score is calculated

Each selector component adds to one of four columns:

CategoryColumnExamples
Inline stylesA (1,0,0,0)style="color:red"
IDsB (0,1,0,0)#header, #nav
Classes, attributes, pseudo-classesC (0,0,1,0).btn, [type="text"], :hover
Elements and pseudo-elementsD (0,0,0,1)div, p, ::before

The universal selector * scores zero. Combinators like >, +, and ~ also score zero. Only the actual components count.

So div > p.class equals one tag + one tag + one class = (0,0,1,2). And :nth-child(2n) is a pseudo-class, so it scores (0,0,1,0), the same weight as a regular class.

This is also why [type="button"] and .btn tie. Attribute selectors sit in the same column as classes.

When specificity matters

  • You added a class rule but the element still shows styles from an older ID rule. The ID wins because (0,1,0,0) beats (0,0,1,0).
  • You are overriding a component library. Bootstrap's .btn-primary is (0,0,2,0) when combined with .btn. A single custom class at (0,0,1,0) will not beat it. Match the depth or add a parent class.
  • A style is not applying and you cannot figure out why. Open DevTools, check the Computed tab. Crossed-out declarations lost to higher specificity somewhere in the CSS cascade.
  • You need a theming layer. Chaining classes like .theme-dark .card gives (0,0,2,0) and stays much easier to override than any ID-based rule.

Common mistakes

Mistake 1: assuming source order always decides

css
.box { color: red; } /* 0,0,1,0 */ div.box { color: blue; } /* 0,0,1,1 - wins despite appearing after */

Source order is only a tiebreaker when specificity scores are equal. If they differ, the higher score wins regardless of order.

Mistake 2: chaining IDs

css
#header #nav #item { padding: 10px; } /* 0,3,0,0 */

Now you need at least four chained classes just to override this one rule. BEM classes like .header__nav-item score (0,0,1,0) and stay easy to work with.

Mistake 3: forgetting attribute selectors score like classes

css
[type="button"] { border: 1px solid; } /* 0,0,1,0 - same as .btn */ button.primary { border: 2px solid; } /* 0,0,2,0 - wins */

Many developers assume attribute selectors are weaker than class selectors. They are not.

Mistake 4: reaching for !important first

In Tailwind projects this creates real maintenance problems. Once one rule uses !important, the next override needs it too. The fix: increase legitimate specificity by adding a parent selector or extra class instead.

Real-world usage

  • Bootstrap: .btn-primary scores (0,0,2,0), beating .btn at (0,0,1,0). Custom themes need to match that compound level.
  • Material-UI: Override components with #id.class or & .MuiButton-root in JSS to reach (0,1,1,0) or (0,0,2,0).
  • Tailwind: Each utility class scores (0,0,1,0). They stack predictably, but conflicts appear when component styles load alongside utilities.
  • Styled-components and CSS Modules: Generate unique class names with a hash to sidestep conflicts entirely, no specificity math needed.

One thing I see repeatedly in production: a developer adds an ID selector to fix a broken override, and six months later nobody dares touch that rule. Stick to classes.

Follow-up questions

Q: What is the specificity of div > p.class?
A: (0,0,1,2). One class and two elements. The > combinator adds zero points.

Q: How does !important fit into specificity?
A: It does not. Browsers place !important rules in a separate pool above normal rules. Within that pool, specificity still applies. Author !important beats normal author rules, but user !important beats author !important.

Q: What does the universal selector * score?
A: Zero. (0,0,0,0). Combinators score the same way. Only actual components such as IDs, classes, and elements count.

Q: Two rules have equal specificity. Which one wins?
A: The one declared later in the stylesheet.

Q: In a shadow DOM, does host specificity affect ::part() styles?
A: No. ::part() flattens to the component's own specificity context. A host ID adds to the exported part's score but does not pierce internal shadow DOM styles. This is Chrome 89+ behavior and trips developers who assume global cascade rules apply inside shadow boundaries.

Examples

Basic: three selectors targeting one element

html
<p id="intro" class="lead">Hello</p>
css
p { color: black; } /* 0,0,0,1 */ .lead { color: navy; } /* 0,0,1,0 */ #intro { color: darkgreen; } /* 0,1,0,0 - wins */

The paragraph is dark green. Each step up the scoring table carries ten times the weight of the column below it. The ID at (0,1,0,0) cannot be beaten by any number of classes stacked on the same element.

Intermediate: overriding a component library

css
/* Bootstrap base */ .btn { padding: 6px 12px; } /* 0,0,1,0 */ .btn.btn-primary { background: #0d6efd; } /* 0,0,2,0 */ /* Your theme override - loads after Bootstrap */ .custom-theme .btn-primary { background: #e63946; } /* 0,0,2,0 - ties, but declared later = wins */

Adding a parent class .custom-theme bumps the rule to (0,0,2,0), matching Bootstrap's compound selector depth. Because your CSS loads after the framework, declaration order breaks the tie in your favor.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?