Back to CSS
CSS
medium
mid

How does CSS specificity and the cascade decide which rule wins?

The cascade resolves conflicts by: origin/importance > specificity > source order. Specificity is a 3-tuple (IDs, classes/attrs/pseudo-classes, type/pseudo-elements). `!important` and inline styles short-circuit the normal flow.

6 min read·~10 min to think through

Browsers pick the winning declaration via this order:

  1. Origin & importance — author normal < author important < user important < UA important. (Yes, !important from the user wins over !important from the author.)
  2. Specificity — a 3-tuple (a, b, c):
  • a = number of ID selectors (#id)
  • b = number of class, attribute ([type=text]), and pseudo-class (:hover) selectors
  • c = number of type (div) and pseudo-element (::before) selectors

Compared lexicographically: (0,1,0) beats (0,0,99).

  1. Source order — among rules of equal weight and specificity, the later one wins.

Special cases:

  • Inline style="…" has specificity higher than any selector but loses to !important from CSS.
  • :where() has specificity 0 — perfect for low-spec resets.
  • :is() takes the highest spec of its arguments.
  • Cascade layers (@layer) override specificity within layer order — earlier-declared layers lose to later ones, regardless of specificity. This is the modern way to organize a stylesheet without specificity wars.

The practical advice: keep selectors flat, prefer one-class-per-rule, use CSS variables for theme variants, and reach for @layer over !important when you need to clearly stratify resets / components / utilities.

Code

css
/* (0,1,0) — class */
.button { color: blue; }

/* (0,1,1) — class + element */
button.button { color: red; }   /* wins over .button */

/* (1,0,0) — id beats any number of classes */
#cta { color: green; }

/* :where() makes resets non-binding */
:where(button) { all: unset; } /* (0,0,0) — easy to override */
Specificity comparisons
css
@layer reset, components, utilities;

@layer reset      { button { padding: 0; } }
@layer components { .btn   { padding: 8px; } }
@layer utilities  { .p-2   { padding: 16px; } }

/* Layer order beats specificity:
   .btn loses to .p-2 because utilities is declared after components. */
Cascade layers — modern stratification

Follow-up questions

  • How does `:where()` differ from `:is()` for specificity?
  • When is `!important` an acceptable choice?
  • How do CSS layers interact with framework styles (Tailwind, MUI)?

Common mistakes

  • Reaching for `!important` to win — usually an indicator of a deeper architecture problem.
  • Long descendant selectors that pile up specificity and trap you.
  • Treating inline styles as the same as a `style` selector — they're higher.

Performance considerations

  • Selector matching cost is rarely the bottleneck; descendant selectors used to matter more, modern engines are fast.

Edge cases

  • `@layer` + `!important` flips order — important declarations cascade in the *reverse* layer order.
  • Shadow DOM has its own scoped cascade; outer styles need `::part()` or CSS custom properties to influence it.

Real-world examples

  • Tailwind CSS uses layers (`@layer base/components/utilities`) under the hood — that's how `mt-4` overrides component defaults predictably.

Senior engineer discussion

Senior signal: discuss design-system layering strategy, scoped vs global CSS, the role of CSS variables for theming, and how Shadow DOM changes the cascade boundaries.