Type something to search...
Understanding CSS Specificity: How the Browser Decides Which Rule Wins

Understanding CSS Specificity: How the Browser Decides Which Rule Wins

You write a rule, save the file, refresh the page, and nothing changes. You open DevTools and see your declaration crossed out, beaten by some other rule you didn't even know existed. Nine times out of ten, the culprit is specificity: the scoring system browsers use to decide which of several conflicting rules applies to an element.

Specificity has a reputation for being mysterious, but it follows simple, predictable rules. Once you understand how the browser keeps score, you'll be able to read any selector and know how strong it is, fix override problems without resorting to !important, and write CSS that's easier to maintain. This guide explains the scoring system, the tricky cases, and modern tools for keeping specificity under control.

Where Specificity Fits

Specificity is only one step in the cascade, the algorithm that resolves conflicts when multiple declarations set the same property on the same element. In order, the browser compares:

  1. Origin and importance: browser defaults, user styles, author styles, and whether a declaration is marked !important.
  2. Context: for example, shadow DOM encapsulation.
  3. Element-attached styles: declarations in a style attribute.
  4. Cascade layers: rules in later @layers beat earlier ones.
  5. Specificity: the subject of this article.
  6. Order of appearance: if everything else ties, the declaration that appears last wins.

So specificity only matters when two declarations are tied on everything above it. In a typical stylesheet with no layers and no !important, that's most conflicts, which is why specificity feels so central.

It's also important to know what specificity doesn't do. It doesn't compete with inheritance. A value inherited from a parent always loses to any declaration that targets the element directly, no matter how weak that selector is:

#page {
  color: #dc2626;
} /* very specific, but inherited */
p {
  color: #0f172a;
} /* weak, but targets the p directly: wins */

The Scoring System

Every selector gets a specificity value made of three components, usually written as (A, B, C):

  • A: ID selectors. Each #id adds one.
  • B: Class-like selectors. Each class (.button), attribute selector ([type="email"]), and pseudo-class (:hover, :focus, :nth-child()) adds one.
  • C: Type selectors. Each element name (div, p, a) and pseudo-element (::before, ::placeholder) adds one.

The universal selector * and combinators (the descendant space, >, +, ~) add nothing.

Let's score some selectors:

SelectorABCSpecificity
p001(0, 0, 1)
.intro010(0, 1, 0)
p.intro011(0, 1, 1)
nav a:hover012(0, 1, 2)
#header .logo img111(1, 1, 1)
input[type="email"]:focus021(0, 2, 1)
ul li::marker003(0, 0, 3)
*000(0, 0, 0)

Comparing values

Values are compared column by column, left to right. The first column with a difference decides. It's not a single number you add up.

#nav a {
} /* (1, 0, 1) */
.site-header .nav .link a {
} /* (0, 3, 1) */

The first selector wins, because A is compared first and 1 beats 0. It doesn't matter that the second has three classes. In fact, no number of classes will ever beat a single ID. People sometimes describe specificity as a base-10 number (ID = 100, class = 10, element = 1), but that's misleading: 11 classes would score 110 and "beat" an ID, which doesn't happen in real browsers.

A worked example

<nav id="main-nav" class="site-nav">
  <a class="nav-link active" href="/blog">Blog</a>
</nav>
a {
  color: #475569;
} /* (0, 0, 1) */
.nav-link {
  color: #0f172a;
} /* (0, 1, 0) */
.site-nav .active {
  color: #2563eb;
} /* (0, 2, 0) */
#main-nav a {
  color: #7c3aed;
} /* (1, 0, 1) */

The link is purple. #main-nav a has an ID, so it outranks everything else. If you wanted .active links to be blue, you'd need a selector with at least one ID and more than zero classes, like #main-nav .active, or better, you'd rewrite the rule that uses an ID in the first place.

Inline Styles and !important

Inline styles (the style attribute) aren't technically part of the specificity calculation. They're handled one step earlier in the cascade, as element-attached styles. In practice, the effect is that inline styles beat any selector in your stylesheet:

<p class="note" style="color: #dc2626">This is red.</p>
#content .note {
  color: #0f172a;
} /* Loses to the inline style */

!important operates at the origin-and-importance step, which comes before everything else. An important declaration beats any normal declaration, including inline styles. When two important declarations conflict, the browser falls back to the usual rules (layers, then specificity, then order) to break the tie.

.note {
  color: #0f172a !important;
} /* Beats the inline style above */

That's why !important feels like a "fix". It skips the specificity fight entirely. The problem is that the only way to override an !important declaration is another !important declaration, so it escalates. Use it sparingly, typically for utility classes that must always win or for accessibility overrides.

The Tricky Pseudo-Classes: :is(), :not(), :has(), and :where()

Modern selectors have special specificity rules that are worth memorizing.

:is(), :not(), and :has()

These take the specificity of their most specific argument. The pseudo-class itself adds nothing.

:is(h1, h2, h3) {
} /* (0, 0, 1): most specific arg is a type */
:is(.title, h2) {
} /* (0, 1, 0): .title wins */
:is(#hero, .title) {
} /* (1, 0, 0): #hero wins, even if the element only matches .title */
a:not(.external) {
} /* (0, 1, 1) */
.card:has(> img) {
} /* (0, 1, 1) */

That third example is a common gotcha. If an element matches only via .title, the selector still carries ID-level specificity because of #hero in the list.

:where()

:where() always has zero specificity, no matter what's inside it.

:where(#hero, .title) {
} /* (0, 0, 0) */
:where(.card) .card-title {
} /* (0, 1, 0): only .card-title counts */

That makes :where() perfect for writing defaults that are easy to override:

/* Base styles anyone can override with a single class */
:where(.prose) :where(h2) {
  margin-block: 2rem 1rem;
  font-size: 1.5rem;
}

Any class selector in your components will beat this without a fight.

:nth-child() with "of S"

:nth-child() counts as a pseudo-class (one B point). If you use the of syntax, the specificity of the selector list inside is added too:

li:nth-child(2) {
} /* (0, 1, 1) */
li:nth-child(2 of .featured) {
} /* (0, 2, 1) */

Specificity and Nesting

Native CSS nesting follows :is() semantics for the parent selector. That means nested rules can end up with higher specificity than you'd expect when the parent is a selector list:

#sidebar,
.panel {
  a {
    color: #2563eb;
  }
}
/* Behaves like :is(#sidebar, .panel) a, specificity (1, 0, 1)
   for both #sidebar a AND .panel a */

If you nest under selector lists that mix IDs and classes, check the result in DevTools, which shows the computed specificity when you hover over a selector in the Styles panel.

Order of Appearance: The Final Tiebreaker

When two selectors have equal specificity, the one that comes later wins:

.button {
  background: #0f172a;
}
.button {
  background: #2563eb;
} /* This wins */

This also applies across files. If components.css is loaded after base.css, rules in components.css win ties. That's why the order of <link> tags and imports matters, and why reordering files in a build can suddenly change how a page looks.

Diagnosing Specificity Problems

When a style isn't applying, here's a quick routine:

  1. Inspect the element in DevTools. Find the property in the Styles panel.
  2. Look for strikethroughs. A crossed-out declaration was overridden. The winning rule appears above it.
  3. Hover over the selectors. Chromium-based browsers and Firefox display the specificity as a tooltip.
  4. Check the Computed panel. Expand the property to see every rule that tried to set it and which won.
  5. Check for layers and !important. If a weaker-looking selector wins, it may be unlayered or important.
  6. Confirm the rule matches at all. If your rule doesn't appear in the Styles panel, the selector isn't matching the element, which is a different problem entirely.

Strategies for Keeping Specificity Low

The goal isn't to win specificity battles; it's to avoid starting them.

Prefer classes over IDs for styling

IDs are great for fragment links and JavaScript hooks, but a single ID in a stylesheet creates a rule that only another ID or !important can override. Classes keep everything on the same level.

Keep selectors flat

.card-title is easier to override than .page .content .card .card-body h3. Methodologies like BEM exist largely to keep specificity flat and predictable: every component element gets its own class, so you rarely need descendant selectors.

Use :where() for base and reset styles

:where(button, input, select, textarea) {
  font: inherit;
  color: inherit;
}

Zero-specificity resets mean any component rule can override them without effort.

Use cascade layers

@layer lets you control priority by grouping rather than selector strength. Styles in a later layer beat earlier layers regardless of specificity:

@layer reset, components, utilities;

@layer components {
  #legacy-widget .title {
    color: #0f172a;
  } /* (1, 1, 0) */
}

@layer utilities {
  .text-accent {
    color: #2563eb;
  } /* (0, 1, 0), but wins: later layer */
}

Avoid !important except for utilities

A reasonable team rule: !important is only allowed in utility classes (like .hidden or .sr-only) that must always win, and in accessibility overrides.

Use @scope for proximity-based styling

@scope lets you limit rules to a subtree without raising specificity, and when two scoped rules tie, the one whose scope root is closer to the element wins. It's supported in Chromium-based browsers and Safari, with Firefox support arriving more recently, so check caniuse before relying on it:

@scope (.card) {
  h3 {
    font-size: 1.25rem;
  } /* specificity (0, 0, 1): the scope root adds nothing */
}

Common Misconceptions

"Specificity is a single number." It's three separate columns compared in order. No amount of classes beats an ID.

"The more specific rule always wins." Only when it's in the same layer, same origin, and neither is !important. Layers and importance come first.

"Inherited styles have specificity." Inherited values have no specificity at all. Any direct rule beats them.

"* adds specificity." It doesn't. * p has the same specificity as p.

"Combinators add specificity." div > p and div p are both (0, 0, 2).

Conclusion

Specificity is a simple scoring system: IDs, then classes, attributes, and pseudo-classes, then elements and pseudo-elements, compared column by column. Inline styles and !important sit outside it, cascade layers sit above it, and order of appearance breaks ties. :is(), :not(), and :has() take their most specific argument, while :where() counts as nothing.

The best CSS rarely has to think about specificity at all. Keep selectors flat, favor classes, use :where() for defaults, and group styles into layers. When something doesn't apply, open DevTools, hover the selectors, and let the numbers tell you exactly what's happening.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading