Skip to content

How to compare CSS selector specificity

Every selector is scored with the specification’s three counters — A for ID selectors, B for classes, attributes and pseudo-classes, C for type selectors and pseudo-elements. The three examples already show the shape of the answer: #app .card:hover > h2 is 1-2-1, :where(.theme) .card:is(.featured, #hero) is 1-1-0 and main article[data-kind="docs"]::before is 0-1-3.

Selectors are validated with the browser’s own parser and scored inside the page, so nothing is uploaded. The line above the map reports how many selectors were read and which one is strongest; the copy block repeats every score as plain text.

  1. Type or paste selectors into the box, one per line. A line that holds a comma-separated list is split into one row per selector, so a pasted selector list is never added up into a single score.
  2. Press Analyze selectors, or keep typing: the map is rebuilt on every keystroke.
  3. Read the cascade map — each row shows the A-B-C score, a bar scaled to the strongest selector and a tag: Strongest, A-B-C, or Invalid selector when the browser rejects the syntax.
  4. Turn on Sort strongest first to reorder the rows from the heaviest score down; selectors with equal scores keep their input order.
  5. Use Copy report for the A-B-C lines in plain text, or Reset to bring back the three examples and switch the sort off.

What the A-B-C score counts, and what it does not

What the three numbers count

A counts ID selectors such as #app. B counts class selectors (.card), attribute selectors ([data-kind="docs"]) and pseudo-classes (:hover, :focus-visible, :nth-child(2n+1)). C counts type selectors (main, article, h2) and pseudo-elements (::before, ::selection). The universal selector * adds nothing to any column.

Functional pseudo-classes follow the specification: :where() contributes zero, while :is(), :not(), :has() and the of list of :nth-child() are scored by their heaviest argument — that is why :is(.featured, #hero) is worth one ID. ::part() and ::slotted() count as one pseudo-element each, and the argument inside them adds nothing.

Comma lists, invalid selectors and the cascade

A comma list on one line is treated as what it is: several selectors. a, b becomes two rows of 0-0-1 each, and a list pasted from a stylesheet is scored selector by selector. Every row is validated with the browser’s own parser first; a line it rejects — a stray combinator, a triple colon — is still listed, tagged Invalid selector, and left out of the strongest verdict, because an invalid selector never reaches the cascade.

The score answers one question: if two of these selectors match the same element, which declaration wins on weight. Layers, !important, inline styles, order of appearance and scope proximity can still override the arithmetic, so a lower score inside an earlier @layer can win.

Local processing and limits

The whole comparison runs in the tab, so the selectors are never sent anywhere. There is no fixed line limit; the map, the verdict line and the copy block are rebuilt together as you type.

The workbench compares text, it does not apply your stylesheet: nothing is matched against a document, and the numbers are the specification’s weights rather than a prediction of the computed style. For a visual check, run the selector in the browser’s inspector on the real page.

Recent tools: