CSS Specificity — Why One Selector Overrides Another

Quick answer

💡CSS specificity is a three-part weight system expressed as (a, b, c): a counts ID selectors, b counts class, attribute, and pseudo-class selectors, and c counts element and pseudo-element selectors. Weights are compared left to right, so a single ID (1,0,0) beats any number of classes (0,99,99). Inline styles rank above all stylesheet rules, and !important overrides everything except another !important with equal or higher specificity.

Error symptoms

  • A rule that appears in the stylesheet is completely ignored by the browser
  • Changing a class color has no effect because an ID rule overrides it
  • Adding !important fixes the problem once but breaks overrides everywhere else
  • A third-party library's styles override your component styles
  • Removing a selector from a chain causes an unexpected style to appear
  • Styles work on a standalone page but break when embedded in a larger layout

Common causes

  • An ID selector in a stylesheet or third-party CSS overrides class-based rules
  • Inline styles set by JavaScript are not being removed before CSS rules apply
  • Chained selectors like div.container > ul.nav > li.item accumulate high specificity
  • !important overuse making future overrides require even more !important
  • :not(), :is(), and :has() pseudo-classes inherit the specificity of their argument
  • @layer ordering: styles in later layers win regardless of selector specificity

When it happens

  • When integrating a design system or UI library with existing stylesheet rules
  • When overriding vendor styles in a CMS theme or third-party widget
  • After refactoring HTML structure that changes the depth of selector chains
  • When migrating from a BEM or utility-first methodology to a different architecture

Examples and fixes

A single ID selector outweighs any combination of class selectors.

ID beats any number of classes

❌ Wrong

<div id="checkout">
  <button class="btn btn-primary">Pay</button>
</div>

.btn.btn-primary {
  background: blue; /* (0,2,0) */
}
#checkout button {
  background: gray; /* (1,0,1) wins */
}

✅ Fixed

<div id="checkout">
  <button class="btn btn-primary">Pay</button>
</div>

.btn.btn-primary {
  background: blue; /* (0,2,0) */
}
#checkout .btn {
  background: blue; /* (1,1,0) — now explicit */
}

The selector #checkout button has specificity (1,0,1) — one ID and one element. The selector .btn.btn-primary has specificity (0,2,0) — two classes. Comparing left to right: 1 beats 0 in the ID column, so the gray background wins regardless of how many classes the button has. The fix is to acknowledge the ID context and chain the class rule under it explicitly, making the intent clear in code.

One !important leads to another, creating a cascade that is impossible to override predictably.

Escaping !important inflation

❌ Wrong

/* Library */
.widget .title {
  font-size: 14px !important;
}
/* Your override */
.page .widget .title {
  font-size: 18px !important;
}

✅ Fixed

/* Use @layer to manage specificity cleanly */
@layer base {
  .widget .title { font-size: 14px; }
}
@layer overrides {
  .widget .title { font-size: 18px; }
}

When two rules both use !important, specificity applies again between them, and the one with higher specificity wins. This creates an escalating arms race. CSS @layer provides a structured alternative: styles in later layers override earlier ones regardless of specificity. This removes the need for !important when the goal is simply prioritizing one set of styles over another.

How the specificity weight system works

Specificity is calculated as a three-part tuple: (a, b, c). The a column counts ID selectors (#nav, #header). The b column counts class selectors (.button), attribute selectors ([type='text']), and pseudo-classes (:hover, :focus, :nth-child()). The c column counts element type selectors (div, p, span) and pseudo-elements (::before, ::after).

Comparison is always left to right. Any non-zero value in the a column beats any combination of b and c values. The classic demonstration is that (1,0,0) — a single ID — beats (0,99,99) — ninety-nine classes combined. This surprises developers who expect large numbers of classes to accumulate enough weight to win.

Several selectors contribute zero specificity in all columns. The universal selector (*), combinators (space, >, +, ~), and the :where() pseudo-class all have zero specificity. :where() is especially useful for resets and utility layers because it lets you write selectors with no specificity penalty, making them easy to override from anywhere in the stylesheet.

:not(), :is(), and :has() use the specificity of their most specific argument. :not(.active) contributes (0,1,0) — the same as .active alone. :is(h1, h2, h3) contributes (0,0,1) — the specificity of the highest-specificity argument, which is h1 as an element. This can be surprising when a developer expects these pseudo-classes to be neutral wrappers.

Inline styles sit above the entire specificity system, at a conceptual (1,0,0,0) level — one column to the left of ID selectors. Only !important declarations override them, and even then, an inline !important beats a stylesheet !important of any specificity.

CSS @layer adds a layer of priority above and below specificity. Rules in later-declared layers win over rules in earlier layers regardless of selector specificity. However, !important inside layers inverts this: !important rules in earlier layers beat !important rules in later layers, which is intentional for allowing user agent styles to have a guaranteed floor. Understanding this inverted !important behavior within layers prevents subtle bugs when mixing third-party stylesheets placed in early layers with your own override layers.

Finding specificity conflicts in DevTools

Open Chrome or Firefox DevTools and inspect the element whose style is not applying. In the Styles panel, you will see all rules that match the element, listed in order from most specific to least specific. Rules that are overridden appear with a strikethrough. The topmost non-overridden declaration wins.

Hover over any selector in the Styles panel in Chrome — a tooltip shows its computed specificity as a three-part value like (0,2,1). This is the fastest way to compare why one rule wins over another without doing the mental math yourself.

The Computed panel is useful for confirming the final resolved value of any CSS property. If a property appears in Computed with a value you did not expect, click the triangle next to it to expand all matching rules and see which one supplied the winning value. The winning rule is shown at the top.

For !important issues, DevTools adds a warning icon next to overridden !important declarations. Look for the shield or warning icon in the Styles panel — it indicates an !important that was overridden by an even more specific !important. This is always a sign that the cascade needs restructuring rather than more !important.

When diagnosing specificity conflicts introduced by a CSS framework or design system, use the Sources panel to find which file a rule lives in. The Styles panel shows the filename and line number next to each rule — clicking it jumps to the source. This lets you trace whether an override is coming from your own stylesheet, a third-party library, an injected inline style, or a browser extension. Once you know the source, you can decide whether to fix it in your code, add a layer override, or remove the conflicting rule from the third-party file if you control it.

For :is() and :has() selectors, DevTools specificity tooltips calculate based on the most specific argument inside the pseudo-class. If you have a complex :has() selector with multiple argument types, the displayed specificity may be higher than you expect. It is worth checking the tooltip explicitly when these pseudo-classes are involved, rather than estimating the specificity mentally.

Strategies to resolve specificity conflicts

The most sustainable fix is to avoid high-specificity selectors in your own code. IDs in CSS are almost never necessary — ID selectors in HTML are useful for JavaScript targeting and for anchor links, but CSS authors should prefer classes. Replacing #nav with [id='nav'] technically has the same specificity, but .nav is the recommended approach that keeps all selector weights in the b column.

When overriding third-party styles, use @layer. Place the third-party stylesheet in an early layer and your overrides in a later layer. Styles in later layers win regardless of specificity, so you do not need to match or exceed the vendor's selector chains. This is the modern, clean solution for design system overrides.

For utility-first architectures like Tailwind, specificity is designed to be minimal and consistent — all utility classes have (0,1,0) specificity. When a component style needs to win over utilities, adding a single wrapper class or using the component class directly is enough. Avoid ID selectors or inline styles as overrides.

:where() is useful for reusable base styles that should always be easy to override. Wrapping a selector in :where() drops its specificity to zero: :where(.card) .title has zero specificity from the .card part, making it trivially overridable by any .title rule anywhere in the sheet. Use :where() in design system base layers and :is() when you need specificity to stick.

BEM naming convention (Block__Element--Modifier) is a structural approach to keeping all selectors at (0,1,0) by relying exclusively on flat single-class selectors. Because every element gets a unique class name that encodes its role, there is no need for nested selectors that chain classes and raise specificity. A .card__title selector is always (0,1,0) regardless of the DOM structure, and .card__title--highlighted is also (0,1,0). This makes all overrides predictable and eliminates specificity escalation across the codebase.

Edge cases: layers, inheritance, and shadow DOM

CSS @layer changes the winner of specificity fights in a fundamental way. Within the same layer, normal specificity rules apply. But between layers, the layer order wins — a lower-specificity rule in a later layer beats a higher-specificity rule in an earlier layer. This is intentional and powerful, but it surprises developers who move existing code into layers expecting it to behave identically.

Inheritance is not specificity. When a property like color cascades from parent to child, it is the inherited value — not a stylesheet rule. A child's own stylesheet rule, even with zero specificity, beats an inherited value. This is why color:red on a body element does not prevent a span inside a paragraph from using a stylesheet rule with color:inherit. Developers sometimes confuse weak specificity with inherited values when diagnosing color or font issues.

Shadow DOM components encapsulate their styles in a separate style scope. External stylesheet rules cannot penetrate shadow DOM, and internal shadow DOM styles cannot affect the outer document, regardless of specificity. CSS custom properties (variables) are the officially supported way to style across the shadow DOM boundary — they inherit through the shadow root. If a web component's styles are not responding to your overrides, check whether it uses shadow DOM.

On mobile, specificity works identically to desktop — the cascade algorithm is the same. However, responsive design often introduces specificity-via-media-query confusion. A media query does not add specificity to the rules inside it. Two rules at the same specificity where one is inside a media query and one is outside: the one appearing later in the source order wins when the query matches, purely due to source order, not because the query adds weight.

Common specificity mistakes

The most widespread mistake is using IDs in CSS selectors. Because IDs must be unique per page, an ID selector can never be reused for multiple elements and immediately creates a high-specificity anchor that forces all overrides to also use IDs or !important. Convert all CSS ID selectors to class selectors. Keep IDs in HTML for JavaScript and anchor links only.

Chaining element type selectors to add weight is another anti-pattern. Writing div.card p.description span.highlight instead of .highlight is an attempt to add weight by specificity. This is fragile because it binds the selector to a specific HTML structure. If the markup changes, the selector breaks. Use flat, descriptive class names and keep selectors as short as possible.

Relying on source order instead of specificity for override behavior is a hidden trap. If two rules have identical specificity, the one that appears later in the source wins. This works until someone reorders files, changes import order, or introduces CSS modules that alter bundle order. Explicit specificity differences are more maintainable than relying on source order.

Forgetting that !important applies to individual property declarations, not entire rules. A rule with three properties and one !important only forces that one property. The other two compete normally. Developers sometimes add !important to a single declaration and then wonder why only part of the rule is applying as expected.

Confusing specificity with @layer ordering is a growing source of bugs as @layer adoption increases. When a file is placed in an early layer, its rules lose to rules in later layers even if the early-layer rules have far higher specificity. A developer who moves a high-specificity rule into a layer without understanding layer ordering may find it suddenly stopped working, even though its selector specificity is unchanged. Always reason about both specificity within a layer and layer order separately — they are two independent dimensions of the cascade.

Best practices for manageable specificity

Keep selector specificity flat and consistent. Aim to write most selectors at the (0,1,0) level — single classes. Add context selectors (nested classes) only when you intentionally need higher specificity to scope a component. Document any selector with specificity above (0,2,0) with a comment explaining why it needs that weight.

Adopt a naming convention that encodes scope. BEM (.block__element--modifier) keeps all selectors at (0,1,0) by using single flat classes. Scoped CSS (CSS Modules, CSS-in-JS) avoids global specificity conflicts entirely. Either approach is more maintainable than deep selector chains.

Use @layer to structure your stylesheet architecture. A typical layer order is: @layer reset, base, components, utilities, overrides. Reset and base layers have the lowest priority. Overrides has the highest. This makes the priority of every rule explicit in the layer declaration, independent of specificity.

Audit !important usage during code review. Every !important should require an explanation. Acceptable uses include overriding inline styles set by a library you cannot modify, and utility classes that must always win (like .hidden { display:none !important }). Unacceptable uses include any !important added to make a rule win over another !important — that signals a cascade problem that needs architectural refactoring.

Use :where() extensively in resets and base stylesheets to declare element defaults at zero specificity. A modern CSS reset written with :where(h1, h2, h3) { margin: 0; } is trivially overridden by any downstream rule, because :where() contributes zero specificity. This is a significant improvement over older resets like Normalize.css, which used bare element selectors at (0,0,1) and occasionally required specific overrides to undo. Zero-specificity resets make the entire reset layer opt-in rather than a floor developers must fight against.

Finally, treat specificity as an architectural decision, not a debugging tool. Choose your selector strategy before writing code — whether that is BEM flat classes, scoped CSS Modules, a @layer architecture, or utility classes — and be consistent. Specificity bugs overwhelmingly arise from mixing strategies within a single codebase without a clear priority model, forcing ad-hoc fixes that accumulate into unmaintainable CSS.

Quick fix checklist

  • Use DevTools hover tooltip to check computed specificity of conflicting selectors
  • Replace ID selectors in CSS with class selectors
  • Avoid chaining element type selectors to increase weight artificially
  • Use @layer to manage third-party override priority without !important
  • Use :where() in base styles to keep them at zero specificity
  • Audit every !important and document why it is necessary
  • Keep selectors flat — aim for (0,1,0) or (0,2,0) as a maximum
  • Convert inherited value confusion to explicit property declarations

Related guides

Frequently asked questions

What is CSS specificity and why does it matter?

Specificity is the scoring system browsers use to decide which CSS rule applies when multiple rules target the same element and property. It matters because without understanding it, developers fight the cascade with !important and deeply nested selectors instead of writing clean, maintainable rules. Understanding specificity lets you predict which rule will win and structure selectors intentionally.

How do I calculate specificity for a selector?

Count ID selectors for column a, class/attribute/pseudo-class selectors for column b, and element/pseudo-element selectors for column c. The selector #nav .menu li::before has specificity (1,1,1,1): one ID, one class, one element, one pseudo-element. Compare two selectors left to right — the first column where one has a higher count wins.

Does !important override inline styles?

A stylesheet !important declaration does beat an inline style without !important. However, an inline style with !important beats a stylesheet !important of any specificity. In practice, both should be avoided. If you control the codebase, remove the inline style via JavaScript instead of escalating to !important.

What specificity does :not() have?

:not() itself contributes zero specificity, but its argument does. :not(.active) has specificity (0,1,0) from .active. :not(#header) has specificity (1,0,0) from the ID. The pseudo-class wrapper is neutral — only its content counts.

What is the difference between :is(), :where(), and :not()?

:is() uses the specificity of its most specific argument — useful when you want specificity. :where() always contributes zero specificity — useful for resets and base styles you want to be easily overridden. :not() uses the specificity of its argument, like :is(). All three are otherwise logically equivalent for matching purposes.

Does @layer affect specificity?

Yes, but not in the traditional sense. Within a layer, normal specificity rules apply. Between layers, the layer order wins regardless of specificity — a rule in a later layer beats a rule in an earlier layer even if the earlier rule has higher specificity. !important inside an earlier layer does beat later layers, which is intentional for user agent and author style interaction.

Can I check specificity in browser DevTools?

Yes. In Chrome DevTools, hover over any selector in the Styles panel and a tooltip shows the computed specificity as a three-part number like (0,2,1). The Styles panel also shows overridden declarations with a strikethrough. The Computed panel shows the final winning value for each property.

Why do utility class frameworks avoid specificity issues?

Tailwind and similar frameworks generate single-class selectors with specificity (0,1,0) for every utility. Because all utilities are at the same level, the one appearing later in the CSS source wins for conflicts. This makes specificity predictable. The tradeoff is that semantic component selectors at the same (0,1,0) level must appear before utilities in the source to be overridden.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.