A rule says the link is blue. Another rule, written later, by someone else, says it is green. The browser paints it blue, and the second author reaches for !important. That reflex is the symptom of not knowing the procedure, because the procedure is short and exact, and once it is in your head the winner is never a surprise. Two specifications hold it: Selectors Level 4 says how a selector is counted into a specificity, and CSS Cascading and Inheritance Level 5 says where that count sits among the other things that decide. Both were read on 2026-10-05 and are quoted below; the CSS specificity calculator does the counting for any selector you paste and shows each piece's contribution.

Three numbers

Selectors Level 4 defines a selector's specificity as three counts, computed "as follows":

  • "count the number of ID selectors in the selector (= A)"
  • "count the number of class selectors, attributes selectors, and pseudo-classes in the selector (= B)"
  • "count the number of type selectors and pseudo-elements in the selector (= C)"
  • "ignore the universal selector"

So #main nav a:hover::before has one ID (A = 1), one pseudo-class (B = 1), and two type selectors plus a pseudo-element (C = 3): written (1,1,3). Combinators (the space, >, +, ~) add nothing; they describe the relationship, not the weight. The three numbers are not digits of one number: "Specificities are compared by comparing the three components in order: the specificity with a larger A value is more specific; if the two A values are tied, then the specificity with a larger B value is more specific; if the two B values are also tied, then the specificity with a larger C value is more specific; if all the values are tied, the two specificities are equal." One ID outweighs any number of classes; eleven classes do not become an ID. Writing specificity as "110" and comparing it as a number is the oldest mistake in the subject.

The specification's own examples table is the quickest calibration:

*               /* a=0 b=0 c=0 */
LI              /* a=0 b=0 c=1 */
UL LI           /* a=0 b=0 c=2 */
UL OL+LI        /* a=0 b=0 c=3 */
H1 + *[REL=up]  /* a=0 b=1 c=1 */
UL OL LI.red    /* a=0 b=1 c=3 */
LI.red.level    /* a=0 b=2 c=1 */
#x34y           /* a=1 b=0 c=0 */
#s12:not(FOO)   /* a=1 b=0 c=1 */
.foo :is(.bar, #baz)
                /* a=1 b=1 c=0 */

And one note that surprises people: "Repeated occurrences of the same simple selector are allowed and do increase specificity." .button.button is (0,2,0), a legitimate if inelegant way to outrank .button.primary without an ID.

The pseudo-classes that count differently

Four functional pseudo-classes take selectors as arguments, and the specification gives each an "evaluation context" rule:

  • "The specificity of an :is(), :not(), or :has() pseudo-class is replaced by the specificity of the most specific complex selector in its selector list argument." So :is(em, #foo) is (1,0,0), whichever of its arguments actually matched, and :not(em, strong#foo) is (1,0,1): negating a selector costs nothing and grants nothing beyond the argument's own weight.
  • "The specificity of an :nth-child() or :nth-last-child() selector is the specificity of the pseudo-class itself (counting as one pseudo-class selector) plus the specificity of the most specific complex selector in its selector list argument (if any)." :nth-child(even of li, .item) is (0,2,0): one for the pseudo-class, one for .item.
  • "The specificity of a :where() pseudo-class is replaced by zero." .qux:where(em, #foo#bar#baz) is (0,1,0); the three IDs inside contribute nothing.

That last rule is the modern answer to the !important reflex. A library that wraps its selectors in :where() ships rules with zero specificity, so any rule the page author writes, however plain, overrides them; a page author who wraps in :is() gets the opposite, a floor set by the heaviest argument. The two were designed as a pair.

Two more corners. Pseudo-elements written with one colon, :before, :after, :first-line, :first-letter, are the CSS2 spelling and still pseudo-elements, so they count in C like ::before. And in shadow DOM, the CSS Shadow Module Level 1 (Editor's Draft, 28 April 2026) says :host() and :host-context() are "a pseudo-class, plus the specificity of its argument", and ::slotted() is "a pseudo-element, plus the specificity of its argument", so :host(.dark) ::slotted(span) is (0,2,2).

Five things the cascade checks first

Specificity is the fifth of six criteria. CSS Cascading and Inheritance Level 5 "sorts declarations according to the following criteria, in descending order of priority":

  1. Origin and importance. The origins, in descending order: transition declarations, important user-agent, important user, important author, animations, normal author, normal user, normal user-agent. "Declarations from origins earlier in this list win over declarations from later origins." This is where !important lives: an important author declaration beats every normal author declaration, and no selector can change that.
  2. Context. Between shadow trees, "for normal rules the declaration from the outer context wins, and for important rules the declaration from the inner context wins."
  3. Element-attached styles. Declarations in a style attribute "take precedence over declarations the same importance that are mapped via style rule." No selector reaches an inline style; only importance does.
  4. Layers. "When comparing declarations that belong to different layers, then for normal rules the declaration whose cascade layer is last wins, and for important rules the declaration whose cascade layer is first wins." Declarations outside any @layer go into "an implicit final layer", so unlayered styles beat layered ones regardless of specificity, which is the whole point of layers: a reset or a framework in an early layer can never win a fight it was not meant to have.
  5. Specificity. "Each declaration has the same specificity as the style rule it appears in. The declaration with the highest specificity wins."
  6. Order of appearance. "The last declaration in document order wins", with imported sheets counted where their @import sits and style attributes "all placed after any style sheets".

So the blue link is blue because the two rules differed at a step above specificity, or tied on specificity and the blue rule came later. Reaching for !important moves the fight to step one, where it will be won, and where the next author will have to follow. Reaching for the calculator finds the step, which is usually enough to fix the rule in place: add a class, drop an ID, or move the rule into the right layer.

Sources