css specificity tutorial cover showing 'CSS Selectors and Specificity Explained (2026)' in bold white lettering on a dark navy grid background.

Quick answer: CSS specificity decides which rule wins when two selectors target the same element: inline styles beat ids, ids beat classes, and classes beat elements. A CSS rule has a selector and a declaration block, and selectors range from simple (element, class, id) to relational (descendant, child, attribute, pseudo-class). inline styles beat ids, ids beat classes, and classes beat elements. When specificity fights you, fix the selector rather than adding more rules.

Quick answer card that answers 'css specificity' in plain language for beginners.
css specificity guide roadmap listing the 5 sections in reading order.

The selector types you actually use

SelectorMatchesExample
pAll paragraphsp { margin: 0 }
.cardElements with class card.card { border: 1px solid }
#heroThe element with id hero#hero { height: 60vh }
a:hoverA link under the pointera:hover { color: teal }
input[type=email]Inputs whose type is emailinput[type=email] { border-radius: 4px }
.card > h2h2 that is a direct child of .card.card > h2 { margin-top: 0 }

Classes are the workhorse. Reserve ids for anchors and for JavaScript hooks, because an id selector is hard to override later. Attribute and pseudo-class selectors let you style by state or property without adding extra classes.

CSS specificity in one table

Specificity is a weight, not a score you add up casually. Compare the counts left to right: ids first, then classes and attributes and pseudo-classes, then elements.

/* weight (id, class, element) */
p                       /* 0,0,1 */
.note                   /* 0,1,0 */
.card .title            /* 0,2,0 */
#main .card .title      /* 1,2,0  — the id wins */
a:hover                 /* 0,1,1 */
:where(.card) p         /* 0,0,1  — :where adds nothing */

Higher weight wins regardless of source order. When the weights tie, the later rule wins. Inline styles beat every selector, and !important beats inline styles and everything else — which is exactly why it becomes a maintenance problem.

css specificity concept map connecting the related topics this guide covers.

Modern selectors and CSS specificity

Three additions to CSS have changed how people write selectors, and all three are widely supported now.

/* :is() matches any of a list, and takes the highest weight inside */
:is(h1, h2, h3) { line-height: 1.2 }

/* :where() matches but adds zero specificity — perfect for resets */
:where(ul, ol) { padding-inline-start: 1.25rem }

/* :has() styles a parent based on its children */
.field:has(input:invalid) { border-color: crimson }
form:has(input:focus) { box-shadow: 0 0 0 3px #99f6e4 }

:is() and :where() both collapse a list of selectors into one. The difference is weight: :where() contributes nothing, so a base reset written with it is trivially overridable. And :has() is the first selector that lets you style backward — a field that contains an invalid input, a form that contains a focused input.

Pseudo-classes versus pseudo-elements

  • Pseudo-classes describe a state or position: :hover, :focus, :first-child, :nth-child(2n), :not(.active).
  • Pseudo-elements create a part of an element: ::before, ::after, ::marker, ::placeholder, ::selection.
li:nth-child(odd) { background: #f8fafc }
li:not(:last-child) { border-bottom: 1px solid #e2e8f0 }
::selection { background: #99f6e4 }
.external-link::after { content: " ↗"; }

When styles do not apply: CSS specificity in practice

A rule that “does nothing” is almost always one of three things: another rule with higher specificity, a typo in the property or value, or a syntax error earlier in the file that discarded the whole rule. Open DevTools, select the element, and read the Styles panel — overridden declarations appear struck through with the winning rule shown above them. That panel answers nine out of ten CSS questions.

Next in this series: the CSS box model, which explains the space around every element. To learn CSS with a mentor on real layouts, Ampersand Academy teaches web development one-to-one.

css specificity guide coverage grid showing every section covered in this article.

How css specificity, the cascade, and inheritance interact

CSS specificity is one layer of a three-layer system. The cascade decides which rule wins when specificity ties. Inheritance passes values from parent to child when no rule targets the child at all. Confusing these three is the source of most “why won’t my style apply?” moments. A color set on body reaches every descendant through inheritance, not through specificity. A margin set on .card wins over one set on div through specificity, not the cascade.

Knowing which layer is fighting you saves time. If DevTools shows no struck-through rule but the property still looks wrong, inheritance is the suspect. If a struck-through rule exists, css specificity is the culprit. If two rules have identical weight and the later one is not winning, check whether an @layer is reordering them — that is the cascade acting above specificity.

The full resolution order, from weakest to strongest: browser default styles, then author styles ordered by layer, then unlayered author styles, then inline styles, then !important in the reverse order. Css specificity only competes within the same layer. A low-specificity rule in an unlayered sheet beats a high-specificity rule inside a named @layer, which surprises almost every developer the first time they see it.

The css specificity trap inside @layer

@layer landed in all major browsers in 2022 and is now a realistic tool for managing design systems. It lets you declare named layers and control their priority order independently of selector weight. That power comes with a specificity gotcha worth understanding before you ship anything.

Layers resolve before specificity. A rule in @layer base loses to any rule in @layer theme, even if the base rule carries an id selector and the theme rule only carries an element selector. Here is a minimal example that catches people off guard:

@layer base, theme;

@layer base {
  #hero { background: navy; }   /* weight 1,0,0 */
}

@layer theme {
  section { background: coral; } /* weight 0,0,1 */
}
/* result: coral wins — layer order beats specificity */

@layer base, theme;@layer base { #hero { background: navy; } /* weight 1,0,0 */ }@layer theme { section { background: coral; } /* weight 0,0,1 */ } /* result: coral wins — layer order beats specificity */

The element selector in @layer theme beats the id selector in @layer base because theme was declared later. Css specificity comparisons only happen between rules that sit in the same layer. Cross-layer, the layer order is final.

The practical rule: keep all rules for a component in the same layer, or deliberately put overrides in a higher layer. Never rely on a higher-specificity selector to win across a layer boundary — it will not.

Unlayered styles beat every named layer

Any rule written outside a named @layer block is treated as belonging to an implicit final layer that sits above all named layers. This means a small utility class written without any layer annotation will override a full design-system theme written inside @layer theme, regardless of css specificity weights. That is intentional — it preserves backward compatibility with existing codebases — but it bites teams that introduce layers into a project that already has styles. Audit unlayered rules before mixing them with layered ones.

!important reverses layer order

!important inside a lower-priority layer beats !important inside a higher-priority layer. The reversal is deliberate: user-agent and user stylesheets use !important to enforce accessibility preferences, and those must win even against author !important declarations. In practice: never use !important inside a named layer to solve a specificity fight. Fix the layer order or the selector instead.

Inherited properties, resets, and css specificity collisions

Inherited properties — color, font-size, line-height, font-family — carry no specificity weight at all. Any authored rule targeting the child directly overrides an inherited value, even a rule with weight 0,0,1. This makes element-level resets effective: button { font: inherit } forces buttons to pick up the page font without needing a high-specificity selector, because the explicit rule beats inheritance regardless of weight.

Resets and normalizers work by targeting elements directly at low specificity. That low weight is a feature, not a bug: it means any author rule wins without needing to fight. The pattern breaks when a component stylesheet uses ids or nested selectors to set typographic properties that should be inherited. Now the component’s high css specificity blocks the reset override. Keep base typographic rules at the lowest possible weight — use :where() if needed — so component styles never have to fight them.

The revert keyword is useful here. font-size: revert rolls the value back to the browser default for that element, skipping all author rules. It respects the cascade but ignores author-level css specificity entirely. Use it inside :where()-wrapped reset blocks to produce a clean baseline any component can override with a single class.

Step-by-step workflow for debugging css specificity conflicts

A repeatable process beats guessing. When a style does not apply, work through these steps in order and stop at the first one that finds the cause.

  1. Open DevTools Styles panel. Select the element. Look for struck-through declarations. A struck-through rule means css specificity or layer order beat it. A missing rule means either a selector mismatch or a parse error earlier in the file.
  2. Read the winning rule’s selector. DevTools shows file and line number. Identify its specificity weight manually: count ids, then classes and attributes, then elements.
  3. Compare weights left to right. If the winning rule has a higher id count, you need to remove the id from the winning selector or add one to yours — do not stack classes to compensate, because 0,3,0 never beats 1,0,0.
  4. Check for @layer boundaries. In the Styles panel, layered rules show their layer name. If the winning rule is in a higher layer, weight is irrelevant — fix the layer structure.
  5. Check for !important. A yellow warning icon appears next to overridden !important declarations. If someone added !important to the winning rule, removing it is the correct fix, not adding your own.
  6. Validate syntax. A missing closing brace or an unrecognised property value silently discards every rule that follows it in the same file. Paste the file into a CSS validator or check the DevTools console for parse warnings.

This workflow surfaces the answer in under two minutes for the vast majority of css specificity problems. Only reach for selector changes after step three confirms that weight is actually the issue.

Use the Computed tab for inheritance problems

When no struck-through rule appears but the value is still wrong, switch to the Computed tab in DevTools. Every resolved property shows its final value and the selector that set it, or the word “inherited” with the ancestor element. This panel bypasses css specificity entirely and shows you the end result of the cascade plus inheritance. Filter by property name to cut through the noise. Inheritance problems are invisible in the Styles panel and obvious in the Computed tab.

Managing css specificity in component-based systems

React, Vue, Svelte, and similar frameworks all encourage encapsulated component styles. Encapsulation strategies affect css specificity in ways that matter when components compose or override each other.

CSS Modules generate scoped class names like .card__3xK9a. Every class still carries weight 0,1,0. Two composed components compete on the same specificity footing, so source order becomes the tiebreaker. The specificity weight is predictable, which is good, but ordering surprises happen when bundlers concatenate files in unexpected order.

Styled-components and similar CSS-in-JS libraries inject styles at runtime and manage insertion order. They typically generate a single class per rule, keeping css specificity flat at 0,1,0 across the board. Overrides work through a prop or through the styled(Component) wrapper, which injects after the base rule. The library handles ordering so you rarely touch css specificity directly — but debugging requires reading injected <style> tags, not source files.

Tailwind and other utility-class systems sidestep css specificity conflicts by applying one class per property. Every class is weight 0,1,0. Conflicts become ordering conflicts, solved by the utility library’s own @layer structure and by using !-prefixed utilities only as a last resort.

Whichever system you use, the underlying css specificity rules are the same. The framework shapes how often you encounter them, not whether they apply. Understanding the weight system lets you debug any framework’s output without memorising framework-specific override patterns.

Real-world css specificity patterns to avoid

Certain patterns appear in production code constantly and cause predictable maintenance problems. Recognising them early prevents weeks of specificity debt.

Qualification by tag. Writing div.card instead of .card adds an element to the weight for no benefit. The rule now only matches divs, and the extra weight means any override must also include an element or use a heavier selector. Strip the tag qualifier: .card is more reusable and easier to override.

Deep nesting for scoping. .page .sidebar .widget .title has weight 0,4,0 and breaks if the HTML structure changes. Scoping with a single parent class — .widget .title at 0,2,0 — is enough. For tighter scoping without weight, use :where(.widget) .title at 0,1,0.

Using ids for styling. An id selector adds 1,0,0 to css specificity. No combination of classes can beat it. If a component later needs a variant, the only escape routes are another id, an inline style, or !important. Reserve ids for JavaScript hooks and anchor targets. Style with classes.

Chaining classes for priority. Writing .btn.btn.btn (the same class three times) raises weight to 0,3,0. This is a sign that something upstream has too much css specificity. Find and fix the upstream rule rather than stacking the class. Browsers allow it, but reviewers will — correctly — reject it.

Here is a before-and-after refactor of a common high-specificity tangle:

/* before — hard to override, fragile structure dependency */
#app .layout .sidebar nav ul li a { color: #334155; }

/* after — same visual result, weight 0,1,1 instead of 1,0,6 */
.sidebar-link { color: #334155; }

/* before — hard to override, fragile structure dependency */ #app .layout .sidebar nav ul li a { color: #334155; }/* after — same visual result, weight 0,1,1 instead of 1,0,6 */ .sidebar-link { color: #334155; }

The refactored version is overridable with any class selector. Nothing about the visual result changes. Css specificity drops from 1,0,6 to 0,1,1, leaving headroom for every future variant or theme override.

One more pattern worth naming: scattering !important across a component to fix css specificity fights that an earlier !important created. Each new !important requires another to beat it. The correct fix is always to remove the first one and reduce the selector weight that made it feel necessary.

/* avoid — escalating !important chain */
.btn { color: white !important; }
.btn-dark { color: black !important; }

/* fix — remove both; control color through a modifier class */
.btn { color: white; }
.btn--dark { color: black; }
/* source order resolves the tie; both have weight 0,1,0 */

/* avoid — escalating !important chain */ .btn { color: white !important; } .btn-dark { color: black !important; }/* fix — remove both; control color through a modifier class */ .btn { color: white; } .btn--dark { color: black; } /* source order resolves the tie; both have weight 0,1,0 */

Flat, class-based selectors at consistent weight keep css specificity manageable as a codebase grows. The goal is a selector graph where any rule can be overridden by one class, not a tower where each override needs a taller ladder.

Frequently asked questions

What is CSS specificity?

Specificity is the weight a selector carries when two rules target the same element. Inline styles beat ids, ids beat classes and attributes, and those beat element selectors. Equal weights are decided by source order.

What is the difference between is() and where()?

Both match any selector in a list, but :is() takes the specificity of its most specific argument while :where() adds zero specificity, which makes it ideal for base styles you want to override easily.

What does the has() selector do?

It styles an element based on what it contains, so you can change a field when it holds an invalid input or a form when one of its inputs has focus.

Should I use id selectors in CSS?

Rarely. An id selector outranks classes and is hard to override, so reserve ids for anchors and JavaScript hooks and style with classes instead.

Why is my CSS rule not applying?

Usually another rule with higher specificity is winning, or there is a typo or a syntax error that discarded the rule. The browser DevTools Styles panel shows which rule won.

Last updated on · Written by