/ โ† CSS Checker Fixes / CSS Specificity
In this guide
  1. How specificity problems show up
  2. How the browser decides which rule wins
  3. Tracing a conflict in developer tools
  4. Cascade layers: controlling order without raising scores
  5. The fix, step by step
  6. A before and after
  7. Platform notes
  8. Scoping styles so they do not leak
  9. Questions and answers

How specificity problems show up

Specificity trouble rarely announces itself. You notice it through habits that build up in a codebase:

The CSS Checker scans for specificity issues such as heavy use of !important and overly strong selectors. Treat its list as a map of where the fights are happening; the rest of this guide is about ending them rather than winning each one.

How the browser decides which rule wins

Specificity is only one step in the cascade. When two declarations set the same property on the same element, the browser compares them in this order and stops at the first difference:

  1. Origin and importance. Your stylesheet beats the browser default; an !important declaration beats a normal one.
  2. Cascade layer. For normal declarations, a later layer beats an earlier one, and styles not in any layer beat all layers.
  3. Specificity. The selector with the higher score wins.
  4. Order of appearance. If everything above is equal, the declaration that comes later wins.

Styles written in an element's style attribute outrank any selector in a stylesheet, which is why inline styles added by JavaScript or a page builder are so hard to override without !important.

Reading a specificity score

A selector's specificity is three numbers, compared from left to right: IDs, then classes (including attribute selectors and pseudo-classes), then element names (including pseudo-elements). The first column that differs decides it; a single ID beats any number of classes.

SelectorScore (IDs, classes, elements)Notes
a0, 0, 1One element
.button0, 1, 0Beats any number of plain element names
nav ul li a:hover0, 1, 4The pseudo-class counts as a class
.card .button0, 2, 0Beats .button alone
#header .button1, 1, 0The ID outranks every class-only selector
:where(#header) .button0, 1, 0Anything inside :where() scores zero
:is(#header, .top) .button1, 1, 0:is() takes the strongest argument
*0, 0, 0The universal selector adds nothing

The same "strongest argument" rule applies to :not() and :has(). Native CSS nesting behaves as if the parent selector were wrapped in :is(), so nesting an ID-based parent quietly gives every child rule an ID's weight.

๐Ÿ’ก Chromium-based developer tools and Firefox show a selector's specificity when you hover over it in the Styles or Rules pane. That is quicker and more reliable than counting by eye.

Tracing a conflict in developer tools

Pick one concrete symptom, such as "the newsletter button is grey instead of green", and trace it:

  1. Right-click the element and choose Inspect.
  2. In the Styles pane, find the property you expected to apply. It will be struck through.
  3. Scroll up the pane to find the declaration that is not struck through. That is the winner, and the file and line are shown next to it.
  4. Hover over both selectors to compare their specificity, and check whether either sits inside an @layer block or carries !important.
  5. Open the Computed pane and expand the property to see every declaration that tried to set it, in the order the browser considered them.

Write down the pair: the rule you wanted and the rule that won. After a handful of these you usually see a pattern, such as a theme file using IDs, a plugin using long chains, or a legacy stylesheet sprinkling !important. That pattern tells you which structural fix to apply first.

โš ๏ธ If the winning declaration comes from an element's style attribute, no amount of selector work will help. Find the script, plugin or builder setting that writes the inline style and change it there.

Cascade layers: controlling order without raising scores

Cascade layers are the most useful tool for specificity problems added to CSS in years, and they are supported in all current major browsers. A layer is a named bucket of styles. You declare the order of the buckets once, and from then on a rule in a later layer beats a rule in an earlier layer, however strong the earlier rule's selector is.

/* Declare the order first, at the very top of the first stylesheet */
@layer reset, vendor, base, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
}

@layer base {
  a { color: #1d4ed8; }
}

@layer components {
  .card a { color: inherit; }
}

@layer utilities {
  .text-danger { color: #b91c1c; }
}

With this structure, .text-danger on a link inside a card is red, even though .card a has higher specificity, because the utilities layer comes later. Specificity still matters inside each layer, but it no longer decides fights between layers.

Putting code you do not control into a layer

You can place a whole imported stylesheet into a layer, which is ideal for frameworks and plugin CSS with aggressive selectors:

@import url("vendor/datepicker.css") layer(vendor);

Every rule in that file now loses to your base, component and utility layers, however many IDs it uses.

Two rules that surprise people

The fix, step by step

Step 1

Find the rules that are fighting

Run the CSS Checker and export its specificity findings, then search your source for !important and for selectors containing #. For each visual bug you know about, inspect the element and note the rule you expected and the rule that actually won. Group the pairs by source, such as theme, plugin, framework or your own files, because the source decides whether you can edit the rule or can only contain it.

Step 2

Read the specificity of each selector

Hover over each selector in developer tools to see its score as three numbers for IDs, classes and elements. Anything with an ID, or with more than three classes, is a candidate for rewriting. Watch for hidden weight: nested Sass that compiles into long chains, :is() or :not() containing an ID, and native nesting under an ID parent. Record the target score for each component, ideally a single class.

Step 3

Declare an order of cascade layers

At the very top of your first stylesheet, add one statement such as @layer reset, vendor, base, components, utilities; so the order is fixed before any other CSS loads. Then wrap existing files in the matching layer, starting with the lowest-risk ones. Leave anything you have not migrated yet unlayered for now, remembering that unlayered rules beat layered ones, and move them in one at a time.

Step 4

Move third-party and reset CSS into low layers

Import framework, plugin and widget stylesheets with @import url(...) layer(vendor); or wrap them in an @layer vendor block, so their heavy selectors lose to your own base and component styles. Wrap resets and element defaults in :where() as well, for example :where(ul, ol) { padding: 0; }, so they score zero and any class can override them. This alone often removes the reason for most of your override rules.

Step 5

Flatten your own selectors

Rewrite component selectors so each targets one class, using a naming convention such as .card, .card__title and .card--featured instead of #content .card h3. Replace IDs in selectors with classes, since IDs belong in anchors and JavaScript hooks. Limit Sass nesting to one level for states and pseudo-elements. Check each rewritten component on every template where it appears, in both desktop and mobile widths, before moving on.

Step 6

Remove !important one group at a time

With layers and flat selectors in place, most !important flags have nothing left to beat. Delete them for one component or one file at a time, reload, and compare the page with a screenshot taken before the change. Keep !important only where it is intentional, such as a utility that must always apply, a reduced-motion override, or an override of an inline style you genuinely cannot change at source.

Step 7

Add lint rules so it stays fixed

Configure Stylelint with selector-max-id set to 0, selector-max-specificity set to something like 0,3,0, selector-max-compound-selectors set to 3 and declaration-no-important turned on with named exceptions. Run it in your editor and in continuous integration so new code cannot reintroduce the patterns. Then run the CSS Checker again and confirm the specificity findings for your own files have dropped, leaving only the vendor code you have deliberately contained.

A before and after

Here is a typical tangle from a small business site, where a promotional button has to fight the theme's link styles:

/* Theme */
#page .entry-content a { color: #2563eb; text-decoration: underline; }

/* Someone's fix */
.promo-box .btn-offer { color: #fff; }                     /* loses: 0,2,0 vs 1,1,1 */
#page .promo-box a.btn-offer { color: #fff; }              /* wins, now 1,2,1 */
.btn-offer.is-disabled { color: #9ca3af !important; }     /* needed to beat the line above */

And the same result using layers and flat selectors:

@layer theme, components, states;

@import url("theme.css") layer(theme);

@layer components {
  .btn-offer { color: #fff; text-decoration: none; }
}

@layer states {
  .is-disabled { color: #9ca3af; }
}

No IDs, no !important, and the disabled state wins because of where it sits, not because of how heavy its selector is. When a new state is needed next year, it goes in the states layer with a single class.

๐Ÿ’ก If you cannot use @import for the theme file, because it is enqueued by a CMS for example, you can still wrap your own CSS in layers. Remember that the theme's unlayered rules will then win, so use this approach only once the theme CSS is layered or you have raised your own rules deliberately.

Platform notes

WordPress and page builders

Themes and builders often use ID-based wrappers and write inline styles for colours and spacing chosen in their settings panels. Set the value in the builder where possible rather than overriding it in CSS. For theme CSS you cannot edit, a child theme stylesheet enqueued after the parent, with selectors matching the parent's weight, is still the most predictable route. More on this in CSS in WordPress themes.

Component frameworks

CSS Modules, scoped styles in Vue or Svelte, and utility-first frameworks all keep specificity low by design, usually at one generated class. Conflicts there tend to come from global stylesheets imported alongside the components, or from the order in which bundled CSS files load. The React and Next.js guide covers load-order issues specific to those builds.

Utility frameworks

Utility classes need to win over component styles, which is why some frameworks offer an "important" mode that adds !important to every utility. Current versions of popular utility frameworks build on cascade layers instead, which achieves the same thing without making the utilities impossible to override later.

Scoping styles so they do not leak

Many specificity fights begin as leaks: a rule written for one area of the site also matches elements elsewhere, and someone raises specificity to stop it. Tighter scoping removes the leak at source.

Scoping and layers work well together: layers settle which group of styles wins, and scoping makes sure each rule only matches what it was written for.

Questions and answers

1. Why is my CSS not being applied?

Usually another rule is setting the same property with a stronger claim. Inspect the element and look in the Styles pane: your declaration will be struck through, and the rule above it that is not struck through is the winner. Compare the two selectors' specificity, check whether either is in a cascade layer or marked important, and check whether the winner is an inline style. Less often, the declaration itself is invalid or the selector does not match the element at all.

2. Is using !important always bad?

No, but it should be rare and deliberate. It is reasonable on utility classes that must always apply, on accessibility overrides such as reduced motion, and occasionally to beat an inline style you cannot change at source. It becomes a problem when it is used to win ordinary fights between your own rules, because the only way to override it is another important declaration. Cascade layers and lower-specificity selectors solve those fights without starting an arms race.

3. How do cascade layers differ from specificity?

Specificity compares selectors: an ID beats a class, a class beats an element name. Cascade layers compare groups of styles: you declare an order such as reset, base, components and utilities, and any normal rule in a later layer beats any normal rule in an earlier layer, whatever their selectors. The browser checks layers before specificity, so layers let you control which group of styles wins without making individual selectors heavier.

4. What does :where() do and when should I use it?

The :where() pseudo-class matches exactly like :is(), but it contributes zero specificity, however many IDs or classes are inside it. That makes it ideal for defaults and resets that you want to be easy to override, such as list padding, link colours inside article text, or base heading sizes. Any class applied later will beat a rule written with :where(), so components can style those elements without raising their own specificity.

5. Should I stop using IDs in CSS?

For styling, mostly yes. A single ID outweighs any number of classes, so an ID in a selector makes that rule very hard to override without adding another ID or an important flag. Keep IDs for what they are good at: fragment links, form label associations and JavaScript hooks. Style the same elements with classes instead. If you must target an element that only has an ID, an attribute selector such as [id="header"] carries only class-level weight.

6. Do cascade layers work in all browsers?

Cascade layers are supported in the current versions of Chrome, Edge, Firefox and Safari, and have been for some time. Very old browser versions that do not understand @layer will ignore the layered rules, so if your analytics show significant traffic from browsers several years out of date, test before relying on them. For most sites the practical question is not support but migration, since unlayered styles beat layered ones until everything has been moved in.

7. Why does my theme update keep breaking my custom CSS?

Your overrides were probably written to beat the theme's selectors at one point in time, with the same or slightly higher specificity. When an update changes a selector, adds a wrapper class or switches a rule to an ID, your override loses. Putting the theme's CSS into an earlier cascade layer, or keeping your changes in a child theme that loads after the parent with deliberately matched selectors, makes your styles win because of order rather than a fragile score.

Find your specificity conflicts

The CSS Checker scans for specificity issues alongside W3C validation, render-blocking and unused CSS, vendor-prefix gaps and stylesheet size, so you can see where rules are fighting before you refactor.

Open CSS Checker Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All CSS Checker Fixes  ·  CSS in WordPress Themes  ·  Fixing Unused CSS  ·  CSS Checker Guide  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.