CSS Specificity Explained (Without the Headache)
Photo by Unsplash on Unsplash
Table of Contents
What is CSS specificity?
> Quick answer: CSS specificity is how the browser decides which rule wins when two rules target the same element and set the same property. Each selector gets a score based on the kinds of parts it uses — IDs count more than classes, which count more than element names. The higher-specificity rule wins, and only when specificity ties does the last rule in the source order break the tie. Understanding the scoring is what turns 'why won't this apply?' into a five-second fix.
So the cascade isn't just 'last rule wins,' which is the mental model that leaves people baffled. Last-one-wins is only the tiebreaker. Before that, the browser compares how specific each selector is, and a more specific selector earlier in your file beats a less specific one later. Once you can eyeball which selector is more specific, the mystery evaporates.
How is specificity actually calculated?
The first and highest column is IDs (`#header`). The second is classes, attribute selectors, and pseudo-classes (`.nav`, `[type="text"]`, `:hover`). The third and lowest is element types and pseudo-elements (`div`, `::before`). You count how many of each a selector contains. So `#header .nav a` is (1,1,1): one ID, one class, one element. `.nav a` is (0,1,1). The ID rule wins because the first column is higher, full stop, no matter what's in the other columns.
The non-carrying part is what surprises people. A selector with eleven classes is (0,11,0), and it still loses to a single ID at (1,0,0), because you compare column by column from the left. You can't out-class an ID. Inline styles sit even above all of this, effectively a fourth column to the left, which is part of why inline styles are so hard to override from a stylesheet.
Why won't my CSS rule apply?
Nine times out of ten, the winning rule is more specific than yours. Maybe it targets `#sidebar .widget` while you wrote `.widget`, so its ID gives it a higher score no matter where each sits in the file. The instinctive fix is to make your selector more specific to match or beat it — add the parent, add a class — but that starts an arms race that makes the stylesheet worse over time.
The better habit is to keep specificity low and even across your codebase, so rules compete on source order rather than escalating scores. When most of your selectors are single classes, the cascade becomes predictable again and 'last one wins' actually holds. I lean on browser devtools for this constantly; the Chrome DevTools features worth knowing piece covers the panels that make specificity conflicts obvious at a glance.
When should you use !important (and when not to)?
The problem is that it doesn't end the fight, it just escalates it. Once you use `!important` to win one battle, the only way to override that later is another `!important`, and soon your stylesheet is a stack of overrides fighting each other with no clear winner. The specificity system you were trying to shortcut is now completely scrambled, and debugging gets harder, not easier. I've inherited codebases like that, and untangling them is genuinely painful.
There are a few legitimate uses — overriding third-party styles you can't edit, or utility classes designed to always win — but for your own code, reaching for `!important` is almost always a sign the real problem is specificity that's crept too high somewhere else. Fix the root cause by lowering the competing selector instead of stacking another override. Save the nuclear option for when you genuinely don't control the other stylesheet.
How do you keep specificity manageable?
The main habit is to style with classes and keep selectors flat. Prefer a single class like `.card-title` over a deep chain like `#main .card .header h2`. Flat, class-based selectors all score the same, so they compete on source order, which is predictable and easy to reason about. Avoid styling by ID where you can, since IDs spike specificity and are hard to override later. This is the discipline behind naming systems like BEM.
Modern CSS adds an escape hatch worth knowing: the `:where()` pseudo-class always has zero specificity, so anything wrapped in it is trivially overridable — great for base styles you want to set but never have fight your components. Its sibling `:is()` behaves the same for matching but takes the specificity of its most specific argument, so they're not interchangeable. Between flat class selectors and `:where()` for defaults, you can keep a whole stylesheet low and even, which is the state where CSS stops surprising you. If you're also wrangling layout on top of this, the flexbox vs grid guide pairs well with a tidy specificity setup.
How do modern CSS features change the specificity game?
The biggest is cascade layers, written with `@layer`. They let you define an explicit order of precedence between groups of styles, so a rule in a later layer beats one in an earlier layer regardless of specificity. That means you can drop a low-specificity base layer, a components layer, and a utilities layer, and know utilities always win without a single `!important`. It moves the decision from accidental specificity scores to a deliberate order you set.
Alongside that, the `:where()` pseudo-class I mentioned earlier gives you zero-specificity selectors on demand, which is perfect for resets and defaults you never want fighting your real styles. Pair `:where()` for defaults with `@layer` for structure and most specificity conflicts stop happening before they start.
MDN's reference on specificity is the page I keep bookmarked when I need to double-check how a selector scores. But the more I lean on flat classes, layers, and `:where()`, the less I have to consult it — the whole point of these features is to make specificity something you configure once rather than debug repeatedly. That's the shift worth making: stop fighting the cascade and start arranging it.
Frequently Asked Questions
How is CSS specificity calculated?
It's scored in three columns: IDs, then classes/attributes/pseudo-classes, then elements/pseudo-elements. You count each type in a selector and compare column by column from the left. A higher column always wins, so one ID beats any number of classes.
Why is my CSS not being applied?
Usually a more specific rule is overriding yours. Inspect the element in browser devtools — the styles panel shows the winning rule and crosses out the losers, which tells you exactly what to beat. The [Chrome DevTools guide](/blog/chrome-149-devtools-features-worth-knowing-2026) covers the panels for this.
Does an ID always beat a class in CSS?
Yes. Specificity compares column by column from the left, and IDs sit in a higher column than classes, so a single ID beats any number of classes. That's why relying on IDs for styling makes rules hard to override.
Is it bad to use !important in CSS?
Usually, yes, for your own code. It overrides normal specificity but can only be overridden by another `!important`, which escalates into a tangled mess. It's mainly justified for overriding third-party styles you can't edit.
How do I stop fighting CSS specificity?
Keep selectors flat and class-based so they score the same and compete on source order. Avoid styling by ID, and use the zero-specificity `:where()` pseudo-class for base styles you want to be easily overridable.
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools