Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. CSS specificity: why your style is not applying
Web Development

CSS specificity: why your style is not applying

Specificity is three numbers compared left to right, not one score. Here are the contests that decide it, measured in a browser.

By Max Arthur
Co-Founder & Content Marketer·August 18, 2026·6 Min read
Two competing CSS rules with their specificity scored as three numbers, showing which one wins and why

When two CSS rules set the same property on the same element, specificity decides which one wins. It is not a single score. It is three numbers, compared left to right, and a bigger number on the left beats any number of points on the right.

(ids, classes, elements)

Count the selectors in your rule:

  • ids counts #header
  • classes counts .card, plus attribute selectors like [type="text"] and pseudo-classes like :hover
  • elements counts div, p, plus pseudo-elements like ::before

So #lead is (1,0,0) and .text is (0,1,0). The id wins, and it would still win against a hundred classes, because the comparison never reaches the second number when the first differs. Every result below was measured in a real browser.

The contests, measured

Each row is two rules competing for the colour of one element, with the winner confirmed by reading the computed style:

Rule A Rule B Winner
#lead (1,0,0) .text (0,1,0) A, the id
.a.b (0,2,0) .c (0,1,0) A, two classes beat one
div p.deep (0,1,2) .wrap p (0,1,1) A, same classes, more elements
.first (0,1,0) .second (0,1,0) B, source order breaks the tie

The third row is the one to study. Both rules have exactly one class, so the first two numbers tie at (0,1), and the comparison moves to the third, where two elements beat one. This is why counting "how specific does it feel" fails and counting selectors works.

The fourth row is the rule people forget: when specificity ties exactly, the rule written later wins. Not the more detailed one, not the one in the more specific file. The later one. That is why the order of your stylesheet imports matters, and why a rule that works locally can lose after a build reorders things.

!important, and what it actually beats

!important is not part of the three numbers. It sits above them entirely:

Rule A Rule B Winner
.imp { color: ... !important } #winner { color: ... } A, important beats an id
.impA !important .impB !important B, later important wins
style="color: ..." inline .imp !important B, important beats inline

That last row surprises most people. An inline style attribute normally outranks every selector in your stylesheet, and it still loses to a class carrying !important. Measured, the !important colour won.

Two !important declarations fall back to the normal rules between themselves, so the later one wins, and this is where specificity wars come from. Once one rule is important, the only way to beat it is another important rule, and then the only way to beat that one is to be later in the file. The escalation has no natural end, which is why the advice is to avoid starting it rather than to use it carefully.

The legitimate uses are narrow: overriding a third-party stylesheet you cannot edit, and utility classes that are meant to win by design. Everything else is a specificity problem that has a better answer.

:where() and :is()

:where() takes a selector list and contributes zero specificity. It matches without adding anything to the count:

:where(.zero) #t5 { color: rgb(11,11,11); }
#t5             { color: rgb(22,22,22); }

Measured, the second rule won. Both are (1,0,0), because :where(.zero) added nothing at all, so the tie went to source order and the later rule took it.

That makes :where() the tool for writing defaults that are easy to override, which is exactly what you want in a base stylesheet or a component library:

:where(button) { padding: 8px 16px; }

Any single class the consumer writes beats that, with no !important and no escalation.

:is() looks similar and behaves differently: it takes the specificity of its most specific argument. :is(#a, .b) counts as an id. The two are easy to confuse and the difference is the entire point of having both, so the way to remember it is that :where is where specificity goes to zero.

Counting real selectors

The rules above are easy on .card. Real stylesheets are full of selectors where the count is less obvious, and two of them catch people out. Both were measured:

An attribute selector counts as a class.

input[type="text"] { }   /* (0,1,1) */
input              { }   /* (0,0,1) */

The first won. [type="text"] scores in the classes column, not the elements column, which is why form styling so often has contests that look like ties and are not.

:not() contributes the specificity of its argument.

p:not(.x) { }   /* (0,1,1), because .x counts */
.plain    { }   /* (0,1,0) */

The first won. :not() itself adds nothing, but what you put inside it does. So :not(#id) is as heavy as an id, which is a common way to accidentally write an unoverridable rule while trying to write an exception.

The same holds for :has(). These are the modern selectors most likely to make a rule heavier than it looks, because the specificity is hiding inside the parentheses rather than in the visible chain.

For reference, the columns are:

Column Counts
ids #header
classes .card, [type="text"], :hover, :not(.x) via its argument
elements div, p, ::before

Diagnosing it in ten seconds

Before reasoning about any of this, open DevTools and select the element. The Styles panel lists every rule that matched, in order, with losing declarations struck through.

That display answers the question directly, and it distinguishes between the three things that look identical from the outside:

  • The rule is struck through. Specificity. Something else won, and the panel shows you what.
  • The rule is not listed at all. The selector does not match. A typo, a missing class on the element, or the wrong element selected.
  • The rule is listed and applied, but nothing changes. Not a specificity problem. The property is being overridden by layout, inherited by something else, or does not apply to that element, such as width on an inline element.

Working out which of those three you have takes seconds in the panel and a long time by reading CSS. Chrome DevTools for beginners covers finding the panel.

Fixing it without escalating

Once you know a specificity contest is the cause, there are better moves than adding selectors.

Match the winning specificity, do not exceed it. If .card .title beats your .title, write .card .title too and place it later. Adding an id to force it through creates a rule nobody can override next time.

Give the thing its own class. Most specificity fights are a sign that two rules are trying to own the same element. A single-purpose class that no other rule targets removes the contest instead of winning it.

Use cascade layers for third-party CSS. @layer sets precedence between whole groups of styles, independent of specificity:

@layer vendor, app;

Anything in app beats anything in vendor, regardless of how specific the vendor rules are. This is the modern answer to "the library uses ids and I cannot override it", and it is a much better tool than !important for that job.

Keep selectors shallow. A codebase where most rules are a single class rarely has specificity problems, because most contests end up as ties resolved by source order, which is predictable.

Want to see these contests resolve in front of you? Start with the CSS track, or read how to center a div for a set of rules that fail for layout reasons rather than specificity ones.

Keep reading

More from the blog

Three flex children sized unevenly by their content beside three grid children sized equally by their tracks
September 4, 2026·11 min readWeb Development

Flexbox vs Grid: how to choose in ten seconds

Flexbox vs grid: flex sizes items to their content, grid sizes the tracks. Measured, that gave 32px and 229px against three equal 193px columns.

Read more
A single-colon selector targeting an existing element in a state beside a double-colon selector generating a new box
August 17, 2026·10 min readWeb Development

Pseudo-classes vs pseudo-elements: :hover and ::before

CSS pseudo-classes select an element in a state. Pseudo-elements create something never in your HTML, and on an image they silently do nothing.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs MimoDevpuff vs Sololearn
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs freeCodeCamp
Read All Comparisons