Quick Answer

When two rules target the same element, the browser applies the one with higher specificity, scored as three numbers: ids, then classes and attributes and pseudo-classes, then elements and pseudo-elements. Compare left to right, and the first difference decides — one id beats any number of classes. Only if specificity ties does source order matter. Inline styles outrank everything except !important, which should be a last resort rather than a fix.

How Specificity Is Calculated

Specificity is three numbers, usually written as (a, b, c).

  • a — ids. Count the #id selectors.
  • b — classes, attributes and pseudo-classes. .card, [type="text"], :hover, :nth-child().
  • c — elements and pseudo-elements. div, p, ::before.
p                        (0,0,1)
.intro                   (0,1,0)
p.intro                  (0,1,1)
#header                  (1,0,0)
#header .nav a:hover     (1,2,1)
* , inherited styles     (0,0,0)

Compare from the left. The first column that differs decides it, and nothing in a later column can compensate.

That is the part people find counter-intuitive: one id beats any number of classes. A selector with ten classes scores (0,10,0) and still loses to a single #id at (1,0,0). It is not a total — it is three separate comparisons in priority order.

Note also that the universal selector * and combinators like > and + add nothing, and :not() itself adds nothing though its argument counts.

Specificity Is Only One Step

Before blaming specificity, know the order the browser actually resolves in.

  1. Origin and importance. Author !important beats author normal, which beats browser defaults. (User-agent !important and user styles sit above author !important, which is why accessibility overrides can win.)
  2. Inline styles. A style="" attribute outranks any stylesheet rule of the same importance.
  3. Specificity. The three-number comparison above.
  4. Source order. Only when everything above ties does the last rule win.

That last point matters. "The later rule wins" is only true at equal specificity. Moving a rule to the bottom of your stylesheet will not help if the competing rule is more specific — which is exactly the moment people give up and reach for !important.

One more rule that sits outside this: inheritance always loses to a direct match. A colour inherited from body has zero specificity, so any rule that targets the element itself wins, however weak.

Why !important Is a Trap

!important works, which is the problem. It resolves today's conflict by creating a worse one later.

.button { background: blue !important; }

Now nothing can override that background through normal means. When a variant genuinely needs a different colour, the only tool left is another !important — and once two exist, they are compared by specificity among themselves, so the escalation continues. Large codebases reach a state where every rule shouts and none can be reasoned about.

It is also the hardest kind of CSS to debug, because devtools shows your correct-looking rule struck through with no obvious cause.

There are two legitimate uses. Overriding a third-party stylesheet you cannot edit, and utility classes in a framework that are meant to be final — Tailwind's approach, where .hidden should always hide. Outside those, treat it as a signal that the selector strategy needs fixing.

If you must use it, leave a comment saying why. Six months later nobody remembers, including you.

Debugging a Style That Will Not Apply

Work through this in order rather than guessing.

1. Open devtools and inspect the element. The Styles panel lists every matching rule in priority order, with overridden declarations struck through. If your rule appears struck through, it is a specificity or order problem. If it does not appear at all, the selector is not matching.

2. If the rule is absent, the selector is wrong. A typo, the wrong class name, or a structure assumption that does not hold — .card > p requires p to be a direct child.

3. If it is struck through, compare specificity with the rule that won, using the three numbers.

4. Check for inline styles. JavaScript that sets element.style.something writes an inline style, which beats your stylesheet. This catches people constantly with animation and UI libraries.

5. Check the property is inheritable or set on the right element. Setting text-align on a parent works because it inherits; setting border on a parent does not reach children.

6. Confirm the value is even valid. One invalid declaration is dropped silently while the rest of the block applies, so the rule looks half-broken.

Keeping Specificity Low on Purpose

The best fix for specificity wars is not winning them but avoiding them.

  • Prefer classes. A flat vocabulary of single-class selectors keeps almost everything at (0,1,0), where source order is a predictable tiebreaker.
  • Avoid ids for styling. Use them for anchors and JavaScript hooks. Their (1,0,0) weight cannot be beaten without another id.
  • Do not over-nest. .page .content .card .title scores (0,4,0) and is hard to override. Sass nesting makes this easy to do accidentally — a good reason to keep nesting shallow.
  • Do not qualify unnecessarily. div.card is more specific than .card and no more useful, while also locking the class to one element type.
  • Use :where() for zero-specificity grouping. Everything inside :where() contributes nothing, which is ideal for base styles designed to be overridden.
:where(.prose) p { margin-block: 1em; }    /* (0,0,1) — trivially overridable */
.prose p        { margin-block: 1em; }     /* (0,1,1) — harder to override */

Methodologies like BEM exist mostly to enforce this: one class per component element means specificity stays flat and predictable across a whole codebase.

Frequently Asked Questions

Does a class ever beat an id in CSS? Not on specificity alone — one id at (1,0,0) beats any number of classes, since the columns are compared left to right rather than summed. A class can only win with !important, or an inline style, or if the id rule does not actually match the element.
Why is my CSS not applying even though it is correct? Usually another rule is more specific, or an inline style is overriding it. Inspect the element in devtools: a struck-through declaration means it lost a conflict, while a rule that does not appear at all means the selector is not matching.
Is !important ever acceptable? Occasionally — overriding a third-party stylesheet you cannot edit, or utility classes intended to be final. As a routine fix it causes escalation, because the only way to beat one !important is another, and it makes debugging much harder.
Does the order of stylesheets matter? Only when specificity ties. Source order is the last tiebreaker, so moving a rule later will not help if the competing selector is more specific. That is why reordering often appears to do nothing.
What does :where() do to specificity? It contributes zero specificity regardless of what is inside it, so :where(.a, .b) p scores the same as p alone. It is useful for base or reset styles you want to be trivially overridable. Its sibling :is() takes the specificity of its most specific argument.