What Is CSS Specificity?
CSS specificity is the scoring system browsers use to decide which style rule wins when several rules target the same element. Every selector you write earns a score, and the rule with the highest score takes priority — regardless of where it appears in your file. Understanding this scoring system is the difference between CSS that behaves predictably and CSS that feels haunted. When a style refuses to apply and you cannot figure out why, specificity is almost always the culprit.
The classic mental model is a three-column score written as (a, b, c). The a column counts ID selectors, the b column counts class selectors, attribute selectors, and pseudo-classes, and the c column counts type selectors (like div or p) and pseudo-elements (like ::before). A selector such as #nav .menu li scores (1, 1, 1): one ID, one class, one element. When comparing two selectors, the browser compares column by column from left to right — a single ID beats any number of classes, and a single class beats any number of element selectors.
Many developers learn a shortcut version: IDs are worth 100, classes are worth 10, elements are worth 1, so #nav .menu li is "111". That shortcut is handy, but it is not literally how browsers compute it — eleven classes do not beat one ID. A proper css specificity calculator shows you the real (a, b, c) tuple so you never get fooled by the decimal shorthand.
How to Calculate Specificity: The (a, b, c) System
Let us walk through the counting rules precisely, because the edge cases are where bugs hide. Start with a score of (0, 0, 0) and add points for each part of the selector:
- IDs — each
#headeradds 1 to the a column. Two IDs, like#main #content, score(2, 0, 0). - Classes — each
.buttonadds 1 to the b column. - Attribute selectors —
[type="email"]and[data-open]each add 1 to b, exactly like a class. - Pseudo-classes —
:hover,:focus,:nth-child(2)each add 1 to b. - Type selectors — each
div,span, oraadds 1 to the c column. - Pseudo-elements —
::before,::after,::first-lineeach add 1 to c. The legacy single-colon forms:beforeand:aftercount the same way. - Universal selector —
*adds nothing at all:(0, 0, 0). - Combinators — spaces,
>,+, and~add nothing; they only describe relationships between the parts that do score.
So ul.nav li a:hover breaks down as one class plus one pseudo-class in the b column and three type selectors in the c column: (0, 2, 3). And #sidebar .widget h3::before is (1, 1, 2). If you want to check css selector specificity by hand, write the selector out, circle each scoring part, and tally the columns — or paste your entire file into a css specificity analyzer and let it do the tallying for every selector at once, which is far less error-prone on a real-world stylesheet with hundreds of rules.
Why You Need a Whole-Stylesheet Specificity Analyzer
Single-selector calculators are fine for settling a debate about one rule, but real CSS problems are never about one rule. They are about the distribution of specificity across a whole file: the slow creep of IDs, the duplicated scores fighting silent wars, the !important someone added at 2 AM that now forces every later override to shout louder. A whole-stylesheet css specificity analyzer reveals patterns no single-selector tool can show you.
Consider a typical aging codebase. It starts clean: classes everywhere, scores hovering around (0, 1, 0) to (0, 2, 1). Then a deadline hits, and someone writes #checkout .summary .total at (1, 2, 0) to force a fix. The next developer cannot override it with classes, so they add another ID. Within a year the file is full of 200-point selectors, !important declarations, and rules whose only reason for existing is to out-shout the previous rule. This is called specificity debt, and like all debt, it compounds.
A ranked scan makes the debt visible. When you see your selectors sorted heaviest-first, the outliers jump out immediately: the (2, 3, 4) monster at the top, the cluster of identical (0, 3, 0) scores in the middle that are one reorder away from breaking. That ranked view is the single most useful lens for CSS maintenance, and it is the centerpiece of SpecScope.
:is(), :where(), :not(), and :has() — The Tricky Parts
Modern CSS added functional pseudo-classes that follow special scoring rules, and they confuse even experienced developers. Here is exactly how each one behaves:
:where() always scores zero
:where() is the great eraser. No matter what you put inside the parentheses — even an ID — the whole thing contributes (0, 0, 0). :where(#header) .nav a scores just (0, 1, 1): the ID inside :where() is completely ignored. This makes :where() perfect for writing low-specificity defaults and resets that are easy to override later.
:is(), :not(), and :has() take the best of their arguments
:is(), :not(), and :has() do not add any points of their own — instead, each one adopts the highest specificity among its arguments. :is(.btn, #cta) scores (1, 0, 0) because of the ID, even though a class is also listed. :not(.disabled) scores (0, 1, 0). article:has(img.hero) scores (0, 1, 2): the :has() borrows (0, 1, 1) from img.hero, and the article type selector adds its own point. These can nest, too — :is(:where(#a), .b) scores (0, 1, 0), because the :where() argument contributes zero and the class wins the max.
:nth-child() with "of"
A plain :nth-child(2n+1) is just a pseudo-class: (0, 1, 0). But Selectors Level 4 allows :nth-child(2n of .featured), and in that case the selector after of adds its own specificity on top — so that example scores (0, 2, 0). Very few calculators handle this correctly; SpecScope does.
Getting these rules right matters because a single mis-scored :is() can make you believe a rule is weaker than it is, sending you down a debugging rabbit hole. When you check css selector specificity, always verify the tool handles the functional pseudo-classes — or just use one that was built with them in mind.
Specificity Wars: How They Start and How to End Them
A specificity war happens when two or more selectors with identical (a, b, c) scores target the same element. With equal scores, the winner is decided purely by source order — whichever rule comes later in the file wins. That sounds manageable until you realize it makes your styles order-dependent: moving a rule, concatenating files in a different order, or loading a stylesheet asynchronously can silently flip the outcome.
Wars usually start innocently. Two developers style the same component from different files, both reach for .card .title, and everything works until someone reorders the imports. Or a utility class like .hidden at (0, 1, 0) ties with a component class at (0, 1, 0), and the utility mysteriously stops working depending on which stylesheet loads last.
The fixes, in order of preference: merge the tied rules if they style the same thing; differentiate the scores deliberately (give the rule that should win one more class); or lower everything involved so ties become unlikely. What you should never do is resolve a tie with !important — that just promotes the war to a louder battlefield. SpecScope's audit flags every tie at a meaningful score so you can defuse them before they bite.
!important: The Nuclear Option
The !important declaration outranks all normal specificity — an !important on a (0, 1, 0) rule beats a normal (2, 5, 3) rule. The only thing that beats an !important is another !important with equal-or-higher specificity appearing later. This creates an arms race with no natural ceiling: each override must shout louder than the last, and the stylesheet becomes a shouting match.
There are legitimate uses — user style sheets, utility classes in frameworks, and overriding third-party CSS you cannot edit are the classic cases. But inside your own codebase, !important is almost always a symptom: it means the underlying specificity got too tangled to reason about, so someone reached for the hammer. The professional fix is to audit the file, find the overweight selectors that forced the !important into existence, slim them down, and then delete the hammer. Count your !important declarations regularly; a healthy stylesheet has close to zero.
7 Practical Ways to Lower Specificity
- Drop ancestor chains.
#nav ul.menu li.item a.linkcan almost always become#nav .link— or just.linkif the class is unique. Every removed ancestor is points you will never have to outbid later. - Drop type qualifiers.
div.card→.card,ul.nav→.nav. The element name adds a point and makes the rule brittle against markup changes. - Stop qualifying IDs.
div#headerand#page #headerare pure waste — an ID is already unique. Bare#headermatches exactly the same elements at a lower score. - Use :where() for defaults. Wrap reset and base styles in
:where()so they score zero and any component class overrides them effortlessly. - Prefer classes over IDs for styling. Reserve IDs for JavaScript hooks and fragment anchors. A class-based architecture (BEM, utility classes) keeps the whole file in the
(0, 1, 0)–(0, 3, 0)band where overrides are cheap. - Flatten deep nesting. Preprocessor nesting is convenient but generates long descendant chains. If a nested rule compiles to five compounds, give the target its own class instead.
- Delete dead !important. After slimming the selectors above, re-test: many
!importantdeclarations will turn out to be unnecessary once the scores around them drop.
How to Use SpecScope
Using this css specificity calculator could not be simpler. Paste your entire stylesheet — comments, @media blocks and all — into the analyzer box and press Analyze stylesheet. SpecScope parses every rule (flattening media queries with their context noted), computes the exact (a, b, c) score for each selector including correct :is() / :where() / :not() / :has() handling, and presents a ranked leaderboard from heaviest to lightest.
Above the ranking you get an audit dashboard: total selectors scanned, !important count, ID usage, and detected specificity ties, plus plain-English warning cards explaining each problem and which selectors cause it. Click any selector to expand its token breakdown — a line-by-line explanation of what each part contributes and why — or hit Simplify for a one-click leaner selector with the before/after scores shown side by side. When you are done, export the whole audit as a Markdown report to share with your team or attach to a pull request. Everything runs locally in your browser; your CSS is never uploaded anywhere.