What is CSS selector specificity and how does it work?
TL;DR
Specificity is the selector weight the cascade compares when competing declarations are in the same relevant origin, importance level, encapsulation context, and cascade layer. Count IDs, then classes/attributes/pseudo-classes, then type selectors/pseudo-elements, and compare those columns from left to right. If specificity is equal, scoping proximity and then source order can decide. !important and cascade layers change an earlier cascade stage; they do not add specificity.
What is CSS selector specificity and how does it work?
Specificity is one part of the cascade
The browser does not simply choose the selector with the largest score. It first discards irrelevant rules, then considers cascade origin and importance, encapsulation context, and cascade layer. Only declarations still competing at the same level are compared by specificity. Active animations and transitions also have defined positions in the cascade.
This matters because a low-specificity selector in a later normal layer can beat a high-specificity selector in an earlier layer:
@layer base, components;@layer base {#checkout .button {color: red; /* 1-1-0 */}}@layer components {.button.primary {color: green; /* 0-2-0, but the later normal layer wins. */}}
Normal declarations outside a layer take precedence over normal layered declarations. For important declarations, layer and origin order are intentionally reversed; for example, important user styles can override important author styles. !important should therefore be understood as a change in cascade importance, not an infinitely large specificity score.
Calculating selector weight
Specificity is commonly written as ID-CLASS-TYPE:
- ID: ID selectors such as
#checkout. - CLASS: Class selectors, attribute selectors, and pseudo-classes such as
.button,[aria-current], and:hover. - TYPE: Type selectors and pseudo-elements such as
buttonand::before.
Combinators and the universal selector * add no weight. The columns are compared lexicographically, so 1-0-0 beats 0-20-20; do not treat the value as a base-ten number.
| Selector | Specificity |
|---|---|
button | 0-0-1 |
.dialog button | 0-1-1 |
#checkout .dialog button | 1-1-1 |
button[aria-pressed='true'] | 0-1-1 |
Inline style declarations participate specially in the author cascade and take precedence over normal declarations in author stylesheets. An important author rule can override a normal inline declaration.
Functional pseudo-classes
Some pseudo-classes need special treatment:
:where()and its arguments always contribute0-0-0.:is(),:not(), and:has()contribute the specificity of their most specific selector argument; the pseudo-class itself adds nothing.- CSS nesting follows similar specificity behavior to
:is()for a nested selector list.
This makes :where() useful for defaults that consumers should be able to override easily:
:where(.prose) :where(h2, h3) {margin-block-start: 1.5em;}
Keeping overrides manageable
Prefer classes and intentionally low specificity for reusable components, and use cascade layers to define relationships between resets, components, and utilities. Avoid IDs in reusable selectors and repeated !important escalation. When a declaration loses unexpectedly, browser DevTools can show the winning rule and whether origin, a layer, specificity, or source order caused the result.