Specificity

Learn how CSS calculates a selector's specificity score, memorize the specificity hierarchy, and practice comparing competing selectors to predict which one wins.

Specificity is the scoring system CSS uses to decide which selector 'wins' when the cascade reaches a tie on origin and importance. It is arguably the single most misunderstood part of CSS — but the rule is simpler than it looks once you stop thinking in vague terms like 'more specific' and start counting.

The specificity hierarchy

Every selector gets a specificity score built from four categories, ranked from lowest to highest priority. A selector with even one match in a higher category always beats any number of matches in a lower category — this is not additive across categories, it's a strict hierarchy compared left to right.

• Inline styles — a style attribute directly on an element. Highest specificity of all (beats any selector in a stylesheet). • IDs — #header. Beats any number of classes, attributes, or pseudo-classes. • Classes, attributes, and pseudo-classes — .card, [type="text"], :hover. Beats any number of elements. • Elements and pseudo-elements — div, ::before. The lowest tier.

Writing it as a tuple

Specificity is commonly written as a 4-part tuple (inline, IDs, classes, elements), e.g. (0,0,0,0) for a universal selector *. You compare tuples left to right: the first column where two selectors differ decides the winner, no matter how large the numbers are in later columns.

For example, #nav .item (1 ID, 1 class) is (0,1,1,0). A selector with 50 classes and no ID, like .a.b.c...(50 classes), is (0,0,50,0) — and it still loses to (0,1,1,0), because the ID column is compared first and 1 beats 0 there, regardless of the class count.

Selectors that don't add specificity

The universal selector (*), combinators (>, +, ~, space), and the negation pseudo-class's own contribution :not() add zero specificity themselves — although the selector(s) inside :not(), :is(), and :has() do count toward specificity (using the highest-specificity argument for :is()/:not()/:has()). :where() is a special case: it always contributes zero specificity, no matter what's inside it — useful for writing resettable base styles.

Debugging a real specificity conflict

Say a button's text won't turn white no matter what you write. In DevTools you'd see two competing rules. Walking it through by hand: .card .btn is one ID? No IDs, two classes → (0,0,2,0). #page .btn is one ID, one class → (0,1,1,0). Comparing left to right: ID column, 0 vs 1 — #page .btn wins outright, even though it has fewer classes overall. The fix is not to guess-and-check with !important, but to either raise the specificity of the rule you want to win, or better, lower the specificity of the ID-based rule (e.g. rewrite #page .btn as .page .btn) so classes can compete fairly.

Comparing specificity tuples

Compare tuples left to right, column by column — never add the numbers together across columns. One extra point in a higher column always outranks any number of points in lower columns.

/* selector                         specificity (inline, id, class, element) */
*                                  /* (0,0,0,0) */
li                                 /* (0,0,0,1) */
ul li                              /* (0,0,0,2) */
.item                              /* (0,0,1,0) */
ul .item                           /* (0,0,1,1) */
#sidebar                           /* (0,1,0,0) */
#sidebar .item                     /* (0,1,1,0) */
style="color: red"                 /* (1,0,0,0) — inline, always highest */

Code Examples

Even three chained classes can't out-rank a single ID, because the ID column is compared before the class column.

When specificity ties exactly, source order is the tiebreaker, not the order classes are listed in the HTML class attribute.:where() lets you group selectors for convenience without inflating specificity — handy for base/reset styles meant to be easily overridden.

#submit-btn {
  background: green;    /* (0,1,0,0) */
}

.form .actions .primary {
  background: red;      /* (0,0,3,0) */
}

/* Result: background is green.
   One ID always outranks any number of classes. */
.highlight {
  color: yellow;   /* (0,0,1,0) */
}

.warning {
  color: orange;   /* (0,0,1,0), same specificity */
}

/* On <p class="highlight warning">, whichever rule is
   declared LAST in the stylesheet wins — here, orange. */
:where(.card, .panel) h2 {
  margin-top: 0;     /* specificity contributed by :where() is always 0 */
}

/* Equivalent selector power to just: h2 { margin-top: 0; } (0,0,0,1)
   which makes it trivially easy for other authors to override later. */

Specificity hierarchy

A match in a higher tier always beats any number of matches in a lower tier.

Low specificity
      |
   Element / pseudo-element   e.g. div, ::before
      |
   Class / attribute / pseudo-class   e.g. .card, [type], :hover
      |
   ID   e.g. #header
      |
   Inline style   e.g. style="..."
      |
High specificity

Important Rules

Specificity is compared as a tuple (inline, ID, class, element), left to right — never summed as one number.

Inline styles beat every selector written in a stylesheet.

IDs beat any number of classes, attributes, or pseudo-classes.

Classes/attributes/pseudo-classes beat any number of elements/pseudo-elements.

Equal specificity falls through to source order — last rule wins.:where() always contributes zero specificity;:not(),:is(),:has() count the specificity of their most specific argument.

Why specificity discipline matters

Teams that consistently use single classes for styling (an approach popularized by methodologies like BEM) rarely fight specificity wars, because every selector has roughly the same, low specificity — the cascade then resolves conflicts predictably by source order. Teams that mix IDs, deeply nested selectors, and !important end up with CSS that's nearly impossible to override safely, forcing rewrites instead of small tweaks.

/* Chaining classes to 'out-specify' an ID */ .a.b.c.d.e { color: blue; } #header { color: red; }

/* Lower the ID selector's usage, or match its tier deliberately */ #header { color: red; } #header.override { color: blue; }

No number of chained classes will ever beat a single ID — the comparison is tier by tier, not a running total. Fighting specificity this way only makes selectors harder to read.

div#main.container.active ul.list li.item.selected a { color: blue; }.list-link.is-selected { color: blue; }

An overly long, deeply chained selector produces extremely high specificity, making it hard for any future rule to override without an equally long selector or !important. Prefer short, purposeful class names.

[Multiple Choice] Which selector has the HIGHEST specificity among these four? A. .sidebar .widget .title (three classes) B. #content h2 (one ID, one element) C. article > h2.heading (one element, one class, one element) D. ul li a:hover (three elements, one pseudo-class)

B. #content h2 (one ID, one element) — #content h2 has specificity (0,1,0,1) — one ID and one element. Because it contains an ID, it beats every other option here, none of which contain an ID (the class-heavy .sidebar .widget .title is (0,0,3,0), still lower in the ID column at 0 vs 1).

Rank these three selectors from lowest to highest specificity, then explain which wins if all three target the same element: nav ul li a, .nav-menu a.active, #main-nav .active.

nav ul li a → (0,0,0,4). .nav-menu a.active → (0,0,2,1). #main-nav .active → (0,1,1,0). Lowest to highest: nav ul li a < .nav-menu a.active < #main-nav .active. The winner is #main-nav .active because it has 1 ID, and no other selector has any ID — the ID column decides it immediately, regardless of the other columns.

Count inline, ID, class, and element matches for each and compare as a tuple, left to right.