ScreenshotNeo

BlogGuides

CSS Specificity: How It Works and How to Avoid Common Issues

Learn to calculate CSS specificity, understand how it fits into the cascade, and fix conflicts without escalating selectors.

By the ScreenshotNeo team4 October 20269 min read

CSS specificity decides between declarations only after the cascade has considered earlier criteria such as relevance, origin, importance, and layer precedence. For declarations still in contention, compare selector weights in three columns—IDs, class-like selectors, and types—from left to right. A single ID outweighs any number of classes or types.

To fix a style conflict, first inspect the winning and crossed-out declarations in browser developer tools. Confirm that both rules apply to the same element and property, then check importance and layers before calculating specificity. If specificity is the deciding factor, compare the three columns, including any weight contributed by :is(), :not(), or :has().

1. Where specificity fits in the cascade

Specificity is one step in the cascade, not a universal strength score. The browser first determines which declarations are relevant and resolves precedence criteria such as origin, importance, and cascade layer. Specificity compares the declarations that remain. If specificity ties, scope proximity and then source order can resolve the conflict. MDN’s cascade guide explains the sequence; the CSS Cascading and Inheritance specification defines it.

This is why a selector that appears more specific can lose. Its declaration might be in a lower-precedence layer, have lower importance, or belong to a different cascade origin. A selector cannot fix a conflict that an earlier cascade step already decided.

2. Calculate specificity in three columns

Write a selector’s weight as ID-CLASS-TYPE, sometimes called A-B-C. Compare the columns from left to right; do not add them into a total or read them as a decimal number.

Column What counts Example
ID ID selectors #app adds 1-0-0
Class Classes, attribute selectors, and pseudo-classes .notice, [type="warning"], and :hover each add 0-1-0
Type Type selectors and pseudo-elements p and ::before each add 0-0-1

Combinators such as whitespace, >, +, and ~ add no weight. The universal selector * also adds none. See the MDN specificity guide and Selectors Level 4 for the selector rules.

Worked comparisons

  • #app .notice[type="warning"] p::before is 1-2-2: one ID, one class, one attribute, one type, and one pseudo-element.
  • .card p is 0-1-1; #card p is 1-0-1. If earlier cascade criteria are equal, the latter wins because its ID column is higher.
  • html body main input is 0-0-4; :root input is 0-1-1. The second selector wins the specificity comparison because the class column is compared before the type column.

A comma-separated selector list does not add the weights of all branches together. For a matched element, the matching branch determines the selector’s specificity. For example, in .notice, #banner, an element matched by .notice gets that branch’s weight; the unrelated #banner branch is not added to it.

3. Modern pseudo-classes that change the calculation

:is(), :not(), and :has() do not add a fixed pseudo-class weight. Each takes the weight of its most-specific argument. A high-weight argument can therefore raise the weight even when another, less-specific argument is the one that matches.

/* #app is the highest-weight argument, so this is 1-0-1. */
:is(.notice, #app) p {
  color: red;
}

/* The arguments and :where() contribute zero; p makes this 0-0-1. */
:where(#app) p {
  color: blue;
}

A hidden ID inside :is(), :not(), or :has() can make a rule unexpectedly difficult to override. :where() always has zero specificity, including the specificity of its arguments. It is useful when you want to scope defaults without raising their weight. The Selectors Level 4 specification defines these rules.

4. Check layer and importance before changing selectors

Within the same origin and importance, later cascade layers beat earlier layers for normal declarations. Normal declarations outside explicit layers beat normal declarations inside them. For important declarations, layer order reverses. A layer changes cascade precedence; it does not change the selector’s specificity.

@layer reset, vendor, components, overrides;

@layer vendor {
  .widget .button {
    color: gray;
  }
}

@layer overrides {
  .button {
    color: navy;
  }
}

Here, the normal rule in overrides can win over the more-specific vendor selector because its layer comes later. The layer order is established in the first statement. Be aware that unlayered normal styles outrank layered normal styles, so a vendor stylesheet left unlayered may require a deliberate integration strategy. For important declarations, layer precedence reverses.

Layers let a team set precedence for areas such as resets, third-party styles, components, themes, and overrides without escalating selectors. MDN’s cascade layers guide provides practical examples. Inline styles are not another entry in the three-column calculation: a normal stylesheet selector does not beat an inline normal style just by accumulating IDs or classes. Importance and origin still matter.

5. A practical workflow for diagnosing a conflict

  1. Inspect the element. In browser developer tools, find the applied declaration and the crossed-out rules. Check each rule’s selector, property, value, stylesheet location, and whether its conditions apply.
  2. Confirm the setup. Make sure both declarations target the same element and property, the expected stylesheet loaded, and any media or other conditional rules match.
  3. Compare earlier cascade criteria. Check origin, importance, and layer before doing specificity arithmetic. A crossed-out rule with a higher specificity can still lose at an earlier step.
  4. Calculate the remaining selectors. Count IDs, then classes, attributes, and pseudo-classes, then types and pseudo-elements. Check for hidden weights inside :is(), :not(), and :has().
  5. Resolve a tie. If the weights tie, check whether @scope proximity applies, then check source order. Later source order decides only after the earlier cascade criteria tie.
  6. Make the smallest maintainable change. Prefer a reusable class, an intentional layer order, or a low-specificity scoped default. Avoid appending ancestors or IDs as a reflex. Use !important only when the declaration’s priority is intentional and documented.

6. Avoid common specificity problems

Situation Why it happens Maintainable approach
A long nested selector is hard to override Every class and type in the selector contributes weight, while combinators only add nesting. Use a component class or a deliberately low-weight scope such as :where().
A rule with an ID unexpectedly dominates One ID wins the first specificity column over any number of class or type selectors. Use classes for reusable styling where possible; remove accidental IDs from selector arguments.
A third-party style wins despite a larger selector on your side The rules may be in different layers, or one may be unlayered. Inspect layer and importance first; define a layer strategy for stylesheets you control.
A selector list raises a pseudo-class’s weight :is(), :not(), and :has() use the most-specific argument. Remove an unnecessarily high-weight argument or use :where() when zero weight is intended.
An override works only after adding !important The conflict may be caused by origin, importance, or layers rather than specificity. Inspect the cascade and fix the actual precedence issue; reserve important declarations for deliberate cases.
A later rule does not take effect Source order matters only when earlier cascade criteria, including specificity and applicable scoping, do not decide the result. Compare the full cascade position before moving rules.

7. Runnable example: reproduce and fix an override

Save this as an HTML file and open it in a browser. The ID selector wins over the later class selector. The second example uses :where() to keep a scoped default easy to override.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Specificity example</title>
<style>
  #panel .notice { color: crimson; }
  .notice { color: royalblue; }

  /* Low-specificity scoped default: 0-0-0 before the p type. */
  :where(#panel) p { background: #f1f5f9; }
  #panel .notice { background: white; }
</style>
<div id="panel">
  <p class="notice">This text is crimson; the background is white.</p>
</div>
</html>

To make the color rule easier to override, remove the ID-based selector or express the intended precedence with layers. Do not keep adding selector parts until the desired color appears: that makes future overrides harder to reason about.

8. Browser setup not needed for CSS itself

Specificity is resolved by the browser’s CSS cascade, so a screenshot API does not calculate or repair it. A screenshot can still help document a rendered page before and after a styling change. ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a PNG, JPEG, WebP, or PDF capture.

Or skip the browser setup

For a rendered page capture, call ScreenshotNeo’s API directly. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners are accepted and removed before capture; 60+ known consent platforms, newsletter popups, and chat widgets can be removed, and each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses identify the page verdict and billing status in headers.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month, with no card required.

9. Performance, reliability, and cost considerations

Specificity is a correctness and maintenance concern; it is not a reason to assume a selector needs more parts or that a conflict requires !important. Keep selectors as narrow as the intended styling scope, and use layers to express broad ordering decisions. For reliable debugging, verify that stylesheets and conditions are active before editing a selector. For screenshot-based visual records, account for the target page’s load state and dynamic content; a capture shows a rendered result at a point in time and does not explain the cascade by itself.

If captures are part of a workflow, ScreenshotNeo offers caching with a chosen TTL, async jobs with signed webhooks, bulk capture for up to 100 URLs per call, and a usage API. Pricing is Free: 1,000 per month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free. Every feature is on every plan. See the docs for API options.

10. Troubleshooting checklist

  • My more-specific rule is crossed out. Check origin, importance, and layer precedence before specificity; inspect whether the other rule is unlayered.
  • Adding more classes did not help. If the competing declaration has an ID, it wins the first column. Also check that the declarations are in the same cascade position.
  • The later rule still loses. Source order is a late tie-breaker. Compare specificity, layer, importance, and scope proximity first.
  • :is() made an ordinary rule unexpectedly strong. Its most-specific argument sets the weight. Remove the unintended high-weight argument or use :where() for a zero-weight scope.
  • A stylesheet override cannot beat an inline style. Do not treat inline styles as a large three-column selector. Check importance and whether the style can be changed at its source.
  • The rule never appears among the candidates. Confirm stylesheet loading, selector match, property name, and media or other conditions in developer tools.

11. Frequently asked questions

Does a longer selector always have higher specificity?

No. Combinators and * add no weight. Count selector components in the three columns instead.

Can many classes beat one ID?

No. Specificity is compared lexicographically, so the ID column is decided before class and type counts.

Does :where() make its arguments count?

No. The pseudo-class and everything inside its argument contribute zero specificity.

Should I use !important to fix specificity?

Usually inspect the earlier cascade criteria first. Importance changes cascade priority rather than adding specificity, and can make future overrides harder.

Can a CSS screenshot tool tell me which rule won?

A screenshot records the rendered appearance. Use browser developer tools to identify the winning declaration and inspect the cascade.