CSS Selector Not Matching — Specificity, Cascade, and Scope Fixes
Quick answer
💡A CSS selector that does not match is almost always explained by one of three things: specificity (a more specific rule elsewhere wins), cascade order (an equal-specificity rule appears later in source and wins), or scope (CSS Modules transform class names, or the selector targets the wrong DOM relationship). Open DevTools, find the Computed tab, and look for the struck-through losing rule alongside the winning rule's selector.
Error symptoms
- ✕
Styles applied in DevTools work but writing the same rule in CSS has no effect - ✕
A class rule is overridden by another rule you cannot identify - ✕
Changing a selector from .parent .child to .parent > .child changes which elements match - ✕
Styles work on one page but not another with similar markup - ✕
CSS Modules class names are not applying in the generated HTML - ✕
Adding !important temporarily fixes the issue but introduces new conflicts
Common causes
- •A higher-specificity rule (ID, inline style, or longer class chain) elsewhere overrides the intended rule
- •Source order: two rules with equal specificity — the later one wins, and it may not be yours
- •Wrong combinator: using a descendant selector when a direct child is required, or vice versa
- •CSS Modules: class names are transformed (Button_button_abc123) and the selector references the original name
- •Cascade layers: unlayered styles beat layered styles regardless of specificity; later @layer beats earlier @layer
- •A typo in the class name, attribute value, or pseudo-class spelling — browsers silently ignore unknown selectors
When it happens
- •When adding component styles in a large project with an existing stylesheet
- •After introducing a CSS framework that uses high-specificity resets or utility classes
- •When migrating from a global CSS approach to CSS Modules or a CSS-in-JS system
- •When adding @layer to a project that previously used unlayered styles
- •When nesting selectors using SCSS or native CSS nesting and the compiled output differs from expectation
Examples and fixes
An ID-based rule from a global stylesheet outweighs a component's class-based rule regardless of source order.
ID selector overriding a class rule
❌ Wrong
/* Global stylesheet — high specificity */
#main-content p {
color: #333333;
line-height: 1.5;
}
/* Component stylesheet — loses to ID rule */
.article-body p {
color: #1a1a1a;
line-height: 1.8;
}✅ Fixed
/* Option 1: Match specificity in the component rule */
#main-content .article-body p {
color: #1a1a1a;
line-height: 1.8;
}
/* Option 2: Refactor global rule to use class */
.main-content p {
color: #333333;
line-height: 1.5;
}
.article-body p {
color: #1a1a1a; /* Now wins by source order */
line-height: 1.8;
}CSS specificity is calculated as a three-part score: (a,b,c) where a counts ID selectors, b counts class/attribute/pseudo-class selectors, and c counts type/pseudo-element selectors. #main-content p scores (1,0,1) — one ID, one type. .article-body p scores (0,1,1) — one class, one type. The ID rule wins regardless of source order because its specificity score is higher. The cleanest long-term fix is to replace ID selectors in global stylesheets with class selectors, making specificity parity achievable with class-only selectors in components.
div p matches any nested paragraph at any depth; div > p matches only direct children.
Wrong combinator: descendant vs direct child
❌ Wrong
/* Targets ALL paragraphs inside .card, including nested ones */
.card p {
margin: 0;
font-size: 14px;
}
/* This .card has nested markup */
<div class="card">
<div class="card__body">
<p>Card body text</p> <!-- Matched */
<blockquote>
<p>Quote paragraph</p> <!-- Also matched — unintended */
</blockquote>
</div>
</div>✅ Fixed
/* Target only direct child paragraphs of .card__body */
.card__body > p {
margin: 0;
font-size: 14px;
}
/* Quote paragraph is no longer affected */
.card blockquote p {
font-style: italic; /* Explicit rule for nested case */
}The descendant combinator (space) matches the target element at any nesting depth inside the ancestor — including grandchildren, great-grandchildren, and beyond. The child combinator (>) matches only elements that are direct children of the parent, not deeper descendants. When a component has rich HTML with nested blockquotes, lists, or custom elements inside paragraphs, the descendant selector applies styles to elements that should not be styled. Use the child combinator and write explicit rules for nested cases when the component structure is known and stable.
Unlayered styles beat all @layer styles regardless of specificity. Later @layer declarations beat earlier ones.
Cascade layer specificity override
❌ Wrong
/* Styles written before @layer was introduced — unlayered */
.button {
background: blue;
color: white;
}
/* New component layer — loses to unlayered .button */
@layer components {
.button {
background: green;
color: white;
}
}✅ Fixed
/* Move all styles into layers — then layer order controls precedence */
@layer base, components;
@layer base {
.button {
background: blue;
color: white;
}
}
@layer components {
.button {
background: green; /* Wins — components layer declared after base */
color: white;
}
}The CSS cascade assigns the highest priority to unlayered styles — styles not inside any @layer block. An unlayered .button rule beats any @layer rule for .button regardless of where the @layer appears in the source and regardless of specificity. This is intentional: existing codebases can adopt @layer incrementally without breaking existing rules. But it means that introducing @layer for new components without moving existing styles into a base layer creates confusing precedence. The fix is to declare all layers in order at the top of the stylesheet and move existing styles into an appropriate layer.
Why CSS selectors lose the cascade battle
A CSS selector not matching its intended element means one of three things: the selector does not match the DOM structure, the selector matches but another rule with higher specificity wins, or the selector matches and has equal specificity but appears earlier in source than the winning rule. Understanding which of these applies is the first diagnostic step.
Specificity is the most common cause. Every selector has a specificity score — a three-part number (a,b,c) where a counts IDs, b counts classes, attributes, and pseudo-classes, and c counts type selectors and pseudo-elements. A rule with any ID in its selector (1,_,_) beats all rules with no IDs (0,_,_) regardless of how many classes are present. One ID defeats one hundred classes in specificity.
Inline styles are the highest-specificity non-!important mechanism. An inline style attribute applies with specificity (1,0,0,0) — above all selector-based specificity. Framework-generated styles, JavaScript animations, and server-rendered attributes frequently produce inline styles that override stylesheet rules. The only selector-based way to beat an inline style is !important, which then creates its own escalation problem.
Cascade layers (via @layer) add another dimension to precedence. Within the normal cascade, unlayered styles beat all layered styles regardless of specificity. Among layered styles, later @layer declarations beat earlier ones. A style in @layer utilities beats a style in @layer base even if the base style has higher specificity.
Source order is the tiebreaker when specificity is equal. The last matching rule in source order wins. In projects with many stylesheet imports, the import order determines which stylesheet's rules take effect when specificity ties. Most build tools deterministically order imports, but bundler changes can alter this order and cause previously winning rules to lose.
Using DevTools to trace selector conflicts
Open Chrome DevTools and select the element that is not styled as expected. In the Elements panel, open the Styles tab. Every applied rule is listed in specificity order, with struck-through (crossed-out) rules indicating losers. The first non-struck rule for a given property is the winning declaration. Hover over the winning rule's selector — DevTools shows the source file and line number.
For inherited properties like color and font-size, check the Computed tab. The Computed tab shows the final resolved value for every property, including a filter field to search by property name. Expand a property to see all rules that tried to set it, sorted by their cascade order, with the winner at the top.
To check whether the selector actually matches any elements, type it into the Elements panel search box (Ctrl+F on Windows, Cmd+F on Mac). DevTools highlights all elements matched by the selector. This quickly reveals typos, wrong combinator usage, or incorrect pseudo-class names — if no elements highlight, the selector does not match anything in the current DOM.
For pseudo-class testing — :hover, :focus, :active — you cannot inspect the natural state because changing focus to DevTools removes the pseudo-class. Use the Force Element State feature: in the Styles tab, click the :hov button and tick :hover, :focus, or :active to pin the state while you inspect.
For CSS Modules issues, inspect the element in DevTools and look at the actual class name in the Attributes section of the Elements panel. The class name in the DOM is the transformed version (Button_button_abc123), not the original .button. Your selector must reference the actual DOM class name. If using the CSS Modules API correctly (styles.button), the transformation is handled automatically — a mismatch indicates the module import is failing silently.
Resolving specificity and cascade conflicts
For specificity conflicts caused by ID selectors, the preferred fix is to replace the ID selector with a class selector in the global stylesheet. IDs used for JavaScript targeting (getElementById) should not also be used for CSS specificity — separate concerns by using data-* attributes for JavaScript hooks and classes for styling.
If you cannot modify the winning rule, match its specificity in your component rule. Use the same selector chain: if the global rule is #app .nav a, your override must include #app somewhere: #app .nav .my-link or #app .new-nav a. This is less maintainable but works when refactoring the global rule is not possible.
:where() is a specificity escape hatch. :where(.button) wraps any selector list and reduces the specificity to zero — the selector matches but contributes no specificity. This makes it easy to override. :is() is the opposite — it uses the highest-specificity selector in its argument list. :is(#id, .class) has ID specificity (1,0,0) even when matching a .class element. Be careful with mixed-specificity :is() selectors.
For cascade layer conflicts, declare all layers at the top of the stylesheet in priority order (last declared = highest priority among layers): @layer reset, base, components, utilities. Then move all styles into the appropriate layer. Unlayered styles only beat layered styles when the unlayered styles come from stylesheets that predate your @layer adoption — once everything is in a layer, precedence is fully controlled by layer order.
For CSS Modules issues, ensure the import syntax is correct: import styles from './Button.module.css'. Reference classes as styles.button, not as a string '.button'. The module system transforms the class name to include a hash — if you use a string, the hash is never applied and the selector does not match the DOM class. Some frameworks offer a :global() escape for non-scoped selectors within a module.
Scope, nesting, and cross-browser edge cases
Native CSS nesting was introduced in Chrome 112 and Firefox 117. The & selector refers to the parent selector. Nested rules that do not start with & are treated as if they are in :is(), which uses the specificity of the highest-specificity selector in the parent context. This can be surprising: a nested rule inside a complex parent selector may have higher specificity than you expect.
CSS nesting compilation by PostCSS, Sass, or Lightning CSS transforms nested rules into flat selectors. The transformation algorithm may produce a selector that differs slightly from the native nesting output, especially when combinators or pseudo-elements are involved. Always check the compiled CSS output in DevTools Sources, not just the source file, when debugging nesting specificity.
Attribute selectors can cause unexpected matching or non-matching. [href] matches any element with an href attribute, regardless of value. [href="/home"] requires an exact match — case-sensitive by default. [href="/home" i] adds case-insensitivity. [href*="example"] matches if the attribute value contains the string. Using the wrong operator causes a mismatch that is easy to overlook.
Pseudo-elements (:before, :after, ::placeholder, ::selection) have type-selector specificity (0,0,1). A class rule targeting the pseudo-element — .input::placeholder — has combined specificity (0,1,1), which is lower than an ID rule without a pseudo-element — #form — which has (1,0,0). Inline styles applied via JavaScript to the host element cannot be overridden by pseudo-element selectors.
In Shadow DOM, styles inside a shadow root do not affect the light DOM, and light DOM styles do not penetrate the shadow boundary. Custom elements with shadow roots require ::part() or CSS custom properties to expose styling hooks. Regular class selectors from the page's main stylesheet have no effect on shadow DOM internals.
Recurring selector mistakes in real projects
Reaching for !important as the first response to a specificity conflict is the most damaging pattern. !important defeats all non-!important rules regardless of specificity and source order. The only way to override an !important declaration is with another !important declaration of equal or higher specificity. This creates an escalating arms race. !important should only be used for utility classes that must always win (e.g., .hidden { display: none !important }) or for user stylesheets overriding author styles.
Using attribute selectors with case-sensitive values when the HTML uses mixed case. [type="submit"] does not match type=Submit. HTML attribute values for some attributes are case-sensitive and for others are not. CSS attribute selectors are case-sensitive by default unless the i modifier is used.
Confusing :nth-child() and :nth-of-type(). :nth-child(2) selects the element only if it is both the second child of its parent and matches the rest of the selector. :nth-of-type(2) selects the second sibling of the same type. If a p is the third child (preceded by an h2 and a div), :nth-child(2) does not match it, but :nth-of-type(2) might.
Forgetting that :not() with a complex selector list has the specificity of the highest-specificity selector inside. :not(.active, #selected) has ID specificity (1,0,0) because of #selected, even when the element matches because of .active. This traps developers who use :not() expecting it to be low-specificity.
Using the child combinator (>) when the HTML is not structured as direct parent-child but has wrapping elements inserted by React, Vue, or a CMS. A component may output an extra div between .card and p, breaking .card > p. Inspect the rendered HTML, not the component template, to understand the actual DOM structure.
Writing selectors that win predictably
Keep specificity low by default. Use single-class selectors for components and reserve multi-class or type selector chains for specific overrides. Low-specificity base styles are easy to override with a single class. High-specificity base styles require escalating specificity in every component that needs to deviate.
Avoid ID selectors in stylesheets. Use IDs for JavaScript targeting and anchor links only. Replace ID selectors in global stylesheets with class selectors of the same name. This change alone eliminates the most common specificity conflicts without any other refactoring.
Adopt cascade layers as a project-wide convention when starting a new project. Declare layers in order (reset, base, components, utilities) and never write unlayered styles. This gives you reliable, predictable precedence that does not depend on selector specificity for layer-to-layer overrides.
Use :where() for reset and base styles. :where(h1, h2, h3) { margin: 0 } applies to all headings with zero specificity, so a single class anywhere in the cascade can override it without specificity escalation. This is the recommended pattern for design system base tokens.
Audit and format your CSS regularly using the CSS Formatter tool. A well-formatted stylesheet reveals duplicate selectors, accidental specificity escalation, and inconsistent combinator usage more easily than a minified or disorganised file. Regular audits prevent the accumulation of !important declarations and specificity debt that makes large stylesheets increasingly fragile.
Integrate PostCSS and the stylelint-no-descending-specificity rule into your build pipeline to catch specificity problems at compile time before they reach the browser. This rule flags any case where a selector with lower specificity appears later in the source than a more specific rule targeting the same element and property, which is almost always an unintentional ordering error that would cause the lower-specificity rule to lose the cascade battle silently.
Selector debugging checklist
- ✓Open DevTools Computed tab and find the struck-through losing rule to identify the conflict
- ✓Check the winning rule's selector specificity: IDs beat classes; classes beat type selectors
- ✓Confirm the selector matches DOM elements using DevTools search (Ctrl+F in Elements panel)
- ✓For CSS Modules: inspect the actual class name in the DOM, not the source file class name
- ✓Check cascade layer order: unlayered styles beat all @layer styles regardless of specificity
- ✓Verify combinator: descendant (space) vs child (>) produces very different matches
- ✓Avoid !important; refactor the high-specificity rule or add a cascade layer instead
- ✓Use :where() for base and reset styles to keep specificity at zero and easy to override
Related guides
Frequently asked questions
How do I calculate CSS specificity?
Specificity is a three-part score (a,b,c). Count ID selectors for a, class/attribute/pseudo-class selectors for b, and type/pseudo-element selectors for c. #nav .menu a scores (1,1,1). Compare from left to right — higher a always beats lower a regardless of b and c. Equal a: compare b. Equal a and b: compare c. Equal all three: last in source order wins. Inline styles are (1,0,0,0) — above all selector-based scores.
Why does my class selector lose to another class selector?
When two rules have equal specificity, the one that appears later in the stylesheet source wins. If a global stylesheet loads after your component stylesheet, its rules win at equal specificity. Check the cascade order by looking at the Styles tab in DevTools — rules are listed in specificity order, and within the same specificity, in source order. Move your rule later in the source, increase your selector's specificity, or use cascade layers to control precedence explicitly.
What is the difference between :is() and :where() for specificity?
:is() uses the specificity of the highest-specificity selector in its argument list. :is(#id, .class) has ID specificity (1,0,0) even when matching an element via .class. :where() always has zero specificity regardless of its arguments. Use :where() for reset and base styles that should be easy to override. Use :is() when you want the selector list to be concise and are comfortable with its specificity implications.
How do cascade layers (@layer) change specificity rules?
Cascade layers add a new level to the cascade above specificity. Unlayered styles beat all layered styles regardless of specificity. Among layered styles, later @layer declarations (in source order) beat earlier ones, also regardless of specificity. Within the same layer, normal specificity and source order rules apply. This means a low-specificity rule in a later layer beats a high-specificity rule in an earlier layer.
Why does my CSS Modules selector not apply styles?
CSS Modules transform class names to include a hash: .button becomes .Button_button_abc123 in the generated HTML. If you reference .button as a string in JavaScript or HTML directly, it does not match the transformed class name. Use the module import object: import styles from './Button.module.css' and reference as styles.button. If the class is still not applied, check that the CSS Modules build configuration is correctly enabled and that the file extension is .module.css or whatever your bundler expects.
When should I use > child combinator vs space descendant?
Use the child combinator (>) when you want to match only immediate children, not nested descendants at any depth. Use it when your component's direct children have a known structure and you do not want styles leaking into deeply nested content. Use the descendant combinator (space) when styles should apply at any nesting depth — for example, all links inside an article body regardless of how they are nested. Default to child combinator for component styles and descendant for document-level styles.
How do I override an !important rule?
The only selector-based way to override an !important rule is with another !important rule of equal or higher specificity. This quickly leads to maintenance problems. Better solutions: refactor the original rule to remove !important, use cascade layers to control precedence without !important, or restructure the HTML so the element is not targeted by the original rule. If overriding a third-party stylesheet, consider using a user agent stylesheet override layer or increasing specificity with a wrapper class.
What does a struck-through property in DevTools Styles mean?
A struck-through property in the DevTools Styles tab means that rule was overridden — another rule for the same property won. The rule with the struck-through property still matched the element (the selector is valid), but a different rule has higher specificity or a later source position. Hover over the winning property to see its source file and line. Click the source link to navigate to the winning rule and understand why it has higher precedence.
Does nesting in CSS change selector specificity?
Native CSS nesting uses & to reference the parent selector and produces selectors whose specificity equals the combined specificity of parent and nested selectors. A rule nested inside .card that targets p has combined specificity equal to .card p (0,1,1). Rules without & at the start are implicitly wrapped in :is(), which uses the specificity of the parent context. Pre-processors like Sass compile nesting to flat selectors, so the specificity outcome is the same, but the output is worth checking in DevTools to confirm.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.