The selector that wins isn't always the one written last

A walkthrough of how specificity is calculated, where it trips people up in real stylesheets, and how to read a score before it turns into an !important war.

Work out a selector's specificity with the calculator See how custom properties sit in the cascade with the CSS Variable Architect

Why the link stays blue no matter what you try

Say you write .nav a { color: red; } in one stylesheet and later add #header .nav a { color: blue; } further down. The link stays blue, and it stays blue even if you move the red rule to the very last line of the file. Source order only settles a fight between selectors that carry equal weight. Here it doesn't, because an ID in a selector outweighs any number of element and class names stacked against it, and that weighting is what specificity actually is.

Specificity is the browser's way of deciding which of two competing rules wins when both target the same element. It has nothing to do with which rule was written more recently or which file it lives in. It is a fixed scoring system, and once you know how the score is built, a lot of CSS behaviour that looks arbitrary stops being a mystery.

How the browser scores a selector

Every selector gets scored across four tiers, and the tiers are compared in order. A selector with even one ID always beats a selector built from any number of classes, no matter how many classes are piled on. The tiers, from strongest to weakest, are:

  • Inline styles, the style attribute written directly on an element, which outrank every selector in every stylesheet.
  • ID selectors, written with a hash, such as #header.
  • Classes, attribute selectors and pseudo-classes, such as .nav, [type="text"], and :hover.
  • Element and pseudo-element selectors, such as div, a, and ::before.

Take a selector like nav ul.menu li a:hover. It has one class (.menu), one pseudo-class (:hover), and three elements (nav, ul, li, a is also an element so that's four). Counted properly that's one class-tier point and four element-tier points, with no IDs at all. Add a single #sidebar to that selector and it jumps ahead of any combination of classes and elements you could write against it, regardless of how specific those classes sound. This is where a lot of frustration starts: a selector that reads as very targeted, full of classes and descendant combinators, can still lose to a short selector that happens to include one ID.

The exceptions worth knowing

Two modern pseudo-classes break the pattern in ways that catch people out. The :where() pseudo-class always scores as zero, whatever you put inside its brackets, which makes it useful for writing default styles that are meant to be overridden easily. The :is() pseudo-class, by contrast, takes on the specificity of whichever selector inside it scores highest, so :is(#header, .nav) scores as an ID even though it also matches a class-based selector. Neither behaves the way you'd guess from reading the CSS, and both are common enough in current codebases that they're worth checking.

Where specificity gets away from people

The most common mistake is reaching for an ID selector to style something, usually because the ID was already there for an anchor link or a JavaScript hook. It works the first time. It becomes a problem the moment a designer or another developer needs to override that one rule for a variant, a state, or a different page, and finds that nothing short of another ID or an !important will do it.

Nesting is the second. A preprocessor like Sass makes it easy to write nested rules that read cleanly in the source file but compile into long descendant selectors with several classes chained together. Each level of nesting adds weight, so a component that started as a single .card class can end up compiling to something with four or five class-tier points, and every override written against it afterwards has to match or beat that weight.

The third is the fight against a design system or a utility framework, where component classes and utility classes are pulling in opposite directions. A utility class meant to override padding or colour on one instance often loses to a component class with an equally low score but earlier or later source order, and the fix people reach for is !important, which then has to be matched by another !important further down the file. Once a codebase has more than one or two of these, nobody can predict which rule will win without checking the computed styles in the browser.

Working with specificity instead of fighting it

The most reliable habit is keeping selectors flat and low-scoring by default, one class per rule wherever that's workable, so overrides stay possible without escalating. Methodologies like BEM exist largely to solve this: because every class in a BEM selector scores the same, there's rarely a reason to nest, and specificity stops being a factor in day-to-day styling.

  • Reserve IDs for anchors and JavaScript hooks, so they never enter the specificity conversation at all.
  • Treat !important as a last resort for genuine one-off overrides, since every use raises the bar for whatever needs to override it next.
  • Watch what a preprocessor compiles nesting into, particularly in larger components.
  • Check a selector's score before you ship it if it's fighting something else in the cascade.

That last point is where a calculator earns its place. Working out a score by hand is straightforward for a short selector and genuinely fiddly once :is(), attribute selectors, and several classes are involved in the same rule. Our CSS Specificity Calculator takes a selector and returns its score across the four tiers, which is faster than counting by hand and easier to compare side by side with the rule it's competing against. It works from the same scoring rules the browser applies, but it can still be wrong on unusual syntax it wasn't built to recognise, so treat the number as a starting point for the decision rather than the final word on it.

Custom properties are worth a mention here too, because they sit outside this whole system. A variable declared with -- doesn't carry specificity of its own; only the selector it's declared on does, in the ordinary way. If you're untangling how variable scoping behaves alongside cascade rules, that's covered separately in the CSS Variable Architect guide. For definitions of any of the terms above, the site's Design Glossary covers the wider vocabulary practitioners use around the cascade.

Check a selector's specificity before it fights your next one

Working out why one selector beats another gets easier once you can see the calculation.