Explain CSS specificity. How is it calculated, and how do inline styles and !important fit in?
Advertisement
🧩 Scenario
Architecture Walkthrough
The Three Columns
Specificity is not a single number. It is a three-component tuple, conventionally written (a, b, c), computed from the selector:
- a counts ID selectors, such as
#header - b counts class selectors, attribute selectors, and pseudo-classes, such as
.card,[type="text"],:hover,:nth-child() - c counts type (element) selectors and pseudo-elements, such as
div,a,::before
The universal selector * and combinators (>, +, ~, descendant whitespace) contribute nothing at all. :where() also contributes nothing, and :not(), :is(), and :has() contribute the specificity of their most specific argument rather than any value of their own.
So #sidebar .nav li a:hover is (1, 2, 2): one ID, one class plus one pseudo-class, and two type selectors.
Columns Are Compared Independently
The critical property is that comparison happens column by column from left to right, and the first difference decides the winner. There is no carrying between columns.
This is why (1, 0, 0) beats (0, 11, 0). One ID outranks eleven classes, because the comparison stops at the first column the moment it finds a difference. Writing specificity as "100 versus 011" invites the wrong mental model; treat it as version-number comparison, not arithmetic.
Where Inline Styles and !important Actually Sit
Both of these are commonly mislabelled as specificity levels. They are not. They operate in the cascade, which runs several steps before specificity is ever consulted. Simplified, the browser resolves a conflict in this order:
- Origin and importance. Declarations are bucketed by where they came from (user agent stylesheet, user stylesheet, author stylesheet) and whether they carry
!important. Author!importantbeats normal author styles; user!importantbeats author!important; and user-agent!importantoutranks everything except transition-driven values. Note that the!importantordering inverts the normal origin order, which is how a user's accessibility override can defeat a site's own styles. - Cascade layers. Within an origin,
@layerorder decides, with unlayered styles winning over layered ones for normal declarations. - Inline styles. A
styleattribute beats any selector-based declaration in the same origin and layer. It is not(1,0,0,0)in the specificity tuple; it wins at this separate step. - Specificity. Only now are the tuples compared.
- Source order. If everything above ties, the declaration that appears last in the document wins.
Getting this order right is what separates a candidate who has memorised "IDs are 100" from one who can actually explain why an !important in a low-specificity utility class defeats a heavily chained component selector.
Why This Matters Beyond Trivia
Specificity is the mechanism behind CSS's worst failure mode: the escalation spiral. Someone cannot override a rule, so they chain another class. The next person duplicates an ID. Eventually somebody reaches for !important, and from then on every override in that area needs !important too. The codebase's average specificity ratchets upward and never comes down.
Modern CSS gives you tools to opt out of the spiral rather than win it: :where() for zero-specificity resets, @layer for explicit precedence that ignores specificity between layers, and flat naming conventions like BEM that keep every component selector at a single class.
Key Code Explained
/* (0, 0, 1) — one type selector */
a { color: blue; }
/* (0, 1, 0) — one class */
.link { color: green; }
/* (0, 1, 1) — one class + one type */
a.link { color: teal; }
/* (1, 0, 0) — one ID. Beats the eleven-class rule below. */
#promo { color: red; }
/* (0, 11, 0) — eleven classes, still loses to (1, 0, 0).
Columns are compared left to right with NO carrying. */
.a.b.c.d.e.f.g.h.i.j.k { color: purple; }
/* (1, 2, 2) — #sidebar / .nav + :hover / li + a */
#sidebar .nav li a:hover { color: orange; }
/* Zero-specificity selectors */
* { box-sizing: border-box; } /* (0, 0, 0) */
:where(.btn, #cta) { margin: 0; } /* (0, 0, 0) — args ignored */
/* Specificity of the MOST specific argument, not of the function */
:is(.btn, #cta) { margin: 0; } /* (1, 0, 0) — the #cta wins */
:not(.hidden) { display: block; } /* (0, 1, 0) */
/* Cascade beats specificity: !important is resolved BEFORE tuples */
.util-hidden { display: none !important; } /* (0, 1, 0) but wins */
#app .modal .body .content { display: block; } /* (1, 3, 0), loses */
/* Inline style beats any author selector, at its own cascade step */
/* <div id="x" class="a b c" style="color: hotpink"> -> hotpink */
/* Exact tie -> last one in source order wins */
.badge { color: navy; }
.badge { color: crimson; } /* crimson applies */
The #promo versus .a.b.c…k pair is the single most useful thing to be able to demonstrate. It proves the tuple is compared positionally, not summed, and it immediately invalidates the "specificity is a number like 100 or 011" shorthand that most people learn first.
The :is() versus :where() pair is the modern counterpart. They match identically, but :is() inherits the specificity of its most specific argument while :where() is permanently zero, which makes :where() the right tool for base styles you fully intend to be overridable.
Tradeoffs
| Selector | Specificity | Notes |
|---|---|---|
*, combinators, :where() | (0, 0, 0) | Contribute nothing |
div, a, ::before | (0, 0, 1) | Pseudo-elements count as type, not class |
.card, [href], :hover | (0, 1, 0) | Classes, attributes, pseudo-classes share a column |
#header | (1, 0, 0) | Beats any number of classes |
:is(...), :not(...), :has(...) | Most specific argument | The function itself adds nothing |
Inline style attribute | Separate cascade step | Not a tuple value; beats all selectors in its origin |
!important | Separate cascade step | Resolved before specificity, inverts origin order |
What Interviewers Actually Check
- Whether you can compute the tuple for a compound selector correctly and quickly
- Whether you know the columns are compared independently with no carrying
- Whether you place
!importantand inline styles in the cascade rather than calling them specificity values - Whether you know which selectors contribute zero specificity, and why
:where()exists - Whether you know source order is the final tiebreaker and can say what happens on an exact tie
Follow-Up Questions
- Why does
!importantinvert the origin order so that user styles outrank author styles, and what accessibility problem does that solve? - How does specificity interact with
@layer, and which one is consulted first? - What is the specificity of
:has(#main), and does that surprise you given how:has()is typically used? - If you needed to override a third-party stylesheet you cannot edit, what would you try before resorting to
!important? - Does a CSS transition or animation participate in the specificity comparison, or does it sit somewhere else in the cascade?
Common Candidate Mistakes
- Describing specificity as a base-10 number, which leads directly to the false conclusion that ten or eleven classes can outrank a single ID
- Calling
!important"the highest specificity" instead of recognising it as an importance modifier resolved at an earlier cascade step - Forgetting that
*, combinators, and:where()add nothing, and therefore miscounting selectors that contain them - Counting
::beforeor::afterin the class column when pseudo-elements belong in the type column - Assuming the more specific selector always wins, when cascade origin,
!important, layer order, and inline styles are all resolved before specificity is even consulted
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you compute the specificity tuple for a compound selector like
#sidebar .nav li a:hoveron demand? - Can you explain why the columns never carry over into one another?
- Can you list the cascade steps in order and place specificity correctly within them?
- Can you name at least three selectors that contribute zero specificity?
- Can you explain what happens when two declarations tie on importance, layer, and specificity?
Summary
Specificity is a three-component tuple counting IDs, then classes plus attributes plus pseudo-classes, then type selectors plus pseudo-elements. Comparison runs column by column from left to right, and the first difference ends it. Because there is no carrying between columns, a single ID at (1, 0, 0) beats eleven classes at (0, 11, 0), which is why treating specificity as a decimal number produces wrong predictions.
Inline styles and !important are not specificity values. They are resolved at earlier steps of the cascade: importance and origin first, then cascade layers, then inline styles, and only then specificity, with source order as the final tiebreaker. That ordering explains why a single-class utility carrying !important defeats a four-part component selector, and why user !important styles can override an author's, which is the mechanism behind user accessibility overrides.
The practical value of knowing the calculation exactly is avoiding the escalation spiral, where each failed override adds another class, ID, or !important and the codebase's specificity ratchets permanently upward. Modern CSS provides ways out rather than up: :where() for zero-specificity base styles, @layer for explicit precedence that ignores tuples across layers, and flat single-class conventions that keep every component at the same rank.
Is specificity really a base-10 number like 0-1-2?
No. It is a three-component tuple compared column by column from left to right. There is no carrying, so eleven classes never beat one ID.
Is !important part of specificity?
No. !important operates one level above specificity in the cascade, at the origin and importance step. Specificity is only consulted to break ties between declarations of equal importance.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement