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.
!importantbypasses specificity entirely and creates its own override layer.- Prefer classes over IDs for most rules. They are much easier to override later.
Quick example
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 */<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:
| Category | Column | Examples |
|---|---|---|
| Inline styles | A (1,0,0,0) | style="color:red" |
| IDs | B (0,1,0,0) | #header, #nav |
| Classes, attributes, pseudo-classes | C (0,0,1,0) | .btn, [type="text"], :hover |
| Elements and pseudo-elements | D (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-primaryis (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 .cardgives (0,0,2,0) and stays much easier to override than any ID-based rule.
Common mistakes
Mistake 1: assuming source order always decides
.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
#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
[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-primaryscores (0,0,2,0), beating.btnat (0,0,1,0). Custom themes need to match that compound level. - Material-UI: Override components with
#id.classor& .MuiButton-rootin 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
<p id="intro" class="lead">Hello</p>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
/* 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 readyA concise answer to help you respond confidently on this topic during an interview.