The bug that isn't a bug
You write a CSS rule, it looks correct, and the browser simply ignores it in favor of some other rule you swear is less specific. Nine times out of ten this isn't a browser quirk — it's specificity working exactly as designed, and the rule you thought was "more specific" actually isn't, once you count correctly.
The four-part tuple
The CSS Selectors specification defines specificity as a tuple with four components, compared left to right:
- Inline styles — a
style=""attribute on the element itself. This always outranks any selector in a stylesheet. - ID selectors — each
#idin the selector. - Class, attribute, and pseudo-class selectors — each
.class,[attr], or:hover-style pseudo-class. - Element and pseudo-element selectors — each type selector like
divorp, and each pseudo-element like::before.
Comparison happens left to right: a single ID selector beats any number of classes, and a single class beats any number of type selectors. Only when two selectors tie at one level does the next level get compared.
Worked examples
A few selectors, computed by hand:
#id .class→ one ID, one class →(0, 1, 1, 0)div p→ two type selectors, no ID or class →(0, 0, 0, 2).a.b.c→ three classes on the same element →(0, 0, 3, 0)a:hover→ one type selector (a) plus one pseudo-class (:hover) →(0, 0, 1, 1)
That last one is where people trip up most often: it's easy to forget that the tag name itself counts as an element-level point, separate from the pseudo-class attached to it.
The tricky cases: :not(), :is(), :where()
Modern selectors add a wrinkle. :not() and :is() don't add a specificity point of their own — instead, they contribute the specificity of the most specific selector inside their parentheses. So :not(.foo) counts as one class-level point (from .foo), while :not(#foo) counts as one ID-level point. :where() is the odd one out: no matter what's inside it, :where() always contributes exactly zero specificity — it's designed specifically for writing low-priority default styles that are easy to override.
!important breaks all of this
None of the above matters once !important enters the picture. A declaration marked !important overrides any non-important declaration regardless of specificity, full stop. If two declarations both use !important, then specificity comparison resumes between just those two. This is exactly why !important is worth flagging separately rather than folding into the specificity score — it's a different mechanism entirely, not just "extra points."
Why compute it by hand when you don't have to
Counting IDs, classes, and elements across a compound selector with nested :not() calls is exactly the kind of mechanical task that's easy to get subtly wrong under time pressure — and subtly wrong is worse than obviously wrong, because the bug hides until someone changes an unrelated rule six months later. Running your actual selectors through a spec-accurate calculator and comparing the resulting tuples takes the guesswork out of "why isn't my CSS applying," especially when debugging a cascade with a dozen overlapping rules from different stylesheets.
Frequently Asked Questions
It implements the actual CSS Selectors specification algorithm, including correct handling of :not(), :is(), :where(), attribute selectors, and pseudo-elements — not a rough approximation.
Because 'a' is a type selector (one element-level point) and ':hover' is a pseudo-class (one class-level point). Both points count separately per the CSS spec — this matches every reference specificity calculator, including examples used in MDN's own documentation.
Yes — CSS Specificity Calculator computes the real spec-accurate 4-part tuple for any selector and ranks multiple selectors to show which one wins. It's a one-time $5.99 purchase — no subscription, no account required.
Any selector line containing !important is flagged separately in its own badge, since !important overrides normal specificity comparison entirely rather than adding to the specificity score.
Yes — enter one selector per line and, once you have two or more, the tool ranks them and explains in plain English which selector wins and at which level of the tuple the tie was broken.