Explain CSS specificity. How is it calculated, and how do inline styles and !important fit in?

Beginner10 min interview
Skills tested:
Computing the three-component specificity tuple for a given selectorKnowing that columns are compared independently with no carryingPlacing inline styles and !important correctly in the cascade orderKnowing which selectors contribute zero specificityUnderstanding that source order breaks exact ties

Advertisement

🧩 Scenario

Specificity is what you reason about the moment a style refuses to apply and you are staring at a struck-through declaration in devtools. Knowing the exact calculation is the difference between systematically finding the selector that outranks yours and escalating blindly by chaining another class, duplicating an ID, or dropping in an !important that someone else will have to fight later.

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:

  1. Origin and importance. Declarations are bucketed by where they came from (user agent stylesheet, user stylesheet, author stylesheet) and whether they carry !important. Author !important beats normal author styles; user !important beats author !important; and user-agent !important outranks everything except transition-driven values. Note that the !important ordering inverts the normal origin order, which is how a user's accessibility override can defeat a site's own styles.
  2. Cascade layers. Within an origin, @layer order decides, with unlayered styles winning over layered ones for normal declarations.
  3. Inline styles. A style attribute 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.
  4. Specificity. Only now are the tuples compared.
  5. 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

SelectorSpecificityNotes
*, 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 argumentThe function itself adds nothing
Inline style attributeSeparate cascade stepNot a tuple value; beats all selectors in its origin
!importantSeparate cascade stepResolved 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 !important and 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

  1. Why does !important invert the origin order so that user styles outrank author styles, and what accessibility problem does that solve?
  2. How does specificity interact with @layer, and which one is consulted first?
  3. What is the specificity of :has(#main), and does that surprise you given how :has() is typically used?
  4. If you needed to override a third-party stylesheet you cannot edit, what would you try before resorting to !important?
  5. 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 ::before or ::after in 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:hover on 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.

Frequently Asked Questions

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