How specificity is scored
Every selector gets a three-part score (A, B, C), compared left to right — a higher left column always beats any amount of the columns to its right:
| Column | Counts | Example |
|---|---|---|
| A (IDs) | #id selectors | #nav |
| B (classes) | .class, [attr], :hover | .active, :focus |
| C (elements) | div, ::before | a, li |
So #nav a (1,0,1) beats div.container ul li a (0,1,4) — one ID outranks four classes/elements combined. This is why ID-based styling is a trap: once you’ve used an ID, every override needs an ID too, and your specificity ratchets upward.
The pseudo-class twist
The newer functional pseudo-classes have specific, sometimes surprising weights:
:not(),:is(),:has()add zero themselves but take the specificity of their most specific argument —:is(#id, .class)scores as an ID.:where()is the escape hatch: it always contributes zero specificity, no matter its arguments. Perfect for low-priority defaults you want to be trivially overridable.
Beyond specificity: the full cascade order
Specificity is only one tiebreaker. When the browser resolves competing rules, it considers, in order: origin and importance (is it !important? author vs user-agent?), then cascade layers (@layer), then specificity, then source order. Cascade layers (see FAQ) sit above specificity, which is what makes them so useful — they let a low-specificity rule in a high layer beat a high-specificity rule in a low layer. Inline style="…" outranks selector rules entirely, and only !important can override an inline style.
Debugging a rule that won’t apply
When a style isn’t taking effect, it’s almost always being overridden by something with higher precedence. Paste both selectors into this calculator to compare scores: if one is higher, that’s your answer; if they’re equal, the later one in the stylesheet wins, so check source order; if neither explains it, look for an !important, an inline style, or a cascade layer changing the outcome.