Specificity without the mythology
Three numbers, counted left to right, no carrying. Everything else you have heard about it is folklore, including the points system.
Specificity is a triple. The first number counts ids. The second counts classes, attribute selectors and pseudo-classes. The third counts element names and pseudo-elements. Compare left to right and stop at the first difference. That is the entire algorithm.
The "no carrying" part is the bit that trips people. (0,10,0) does not become (1,0,0). Ten classes lose to one id, and so do a hundred, and so do a thousand. This is not a quirk, it is what makes ids a meaningfully different tier rather than just a heavy class, and it is why using ids in CSS is a decision you inherit for the lifetime of the codebase.
The universal selector * and combinators (>, +, ~, the descendant space) contribute nothing. :where() contributes nothing, and neither does anything inside it, which makes it the tool for writing defaults that any later rule can override without a fight. :is() and :not() take the specificity of their most specific argument, which means :is(h1, #title) is as specific as #title even when it matched the h1, a surprising rule that has caused a lot of confused afternoons.
The practical consequence is a strategy rather than a trick: keep specificity low and flat so that later rules can win by source order rather than by escalation. A codebase where everything is one class is easy to override. A codebase with #app .sidebar ul li a.link is one where every new rule has to be at least that heavy, and that is how stylesheets become unmaintainable, not through volume, but through an arms race.
Use the calculator below on the six preset pairs before you type your own. They are chosen to be surprising. Then go and paste in three selectors from your own project. If any of them is above (0,3,0), you have found something worth simplifying, and simplifying it now is much cheaper than simplifying it in a year.
What a selector is actually worth
parsed, not countedSix pairs that catch people out
Five things against two. The id column decides before the class column is read.
#nav a wins at 1,0,1 against 0,3,2, decided in column a: ids. Once a column differs, nothing in the columns after it is read at all.
One more note on !important, since it belongs here even though it is technically step one. It is not a specificity booster, it moves the declaration into a different origin band entirely. That is one reason an important declaration in a stylesheet beats an inline style, and why the only thing that beats an important author declaration is another important author declaration with higher specificity. Once a codebase has two of them arguing, the only way out is to remove both.
Worth reading
- web.dev: Specificity ↗, Worth reading after the calculator rather than before, so you already have the intuition to attach it to.
- Kevin Powell: more control over the cascade and specificity ↗, Covers :is, :where and @layer in practice, with real refactoring rather than toy examples.
You should now be able to
- Calculate specificity for any selector by hand
- Explain why 100 classes never beat one id
- Use :where() to write a rule that deliberately loses
- Predict which of two selectors wins and by which component
Loading…