Articles

What Is CSS Specificity? The Cascade, Explained

CSS specificity is the scoring system that decides which rule wins when several target the same element. How the weights work and how to keep them low.

Takina Takina · · 5 min read
A close-up of CSS and SVG markup with gradient color values

CSS specificity is the algorithm the browser uses to decide which style rule wins when more than one rule targets the same element and property. Every selector earns a score, the browser compares scores, and the highest one applies. When you write color: red and the text stubbornly stays blue, specificity is almost always the reason — a more specific rule elsewhere is out-scoring yours.

It is one of three factors in the cascade, the “C” in CSS. The other two are source order (later rules beat earlier ones) and importance (!important). Specificity sits in the middle and is where most confusion lives, so it is worth understanding precisely rather than by trial and error.

How the score is calculated

Specificity is usually written as three numbers, (a, b, c), sometimes with a fourth slot for inline styles. Each part counts a different kind of selector:

  • a — ID selectors. Each #id in the selector adds one to this column.
  • b — classes, attributes, and pseudo-classes. Each .class, [type="text"], or :hover adds one here.
  • c — type and pseudo-element selectors. Each element name like div or p, and pseudo-elements like ::before, adds one here.

The universal selector *, and combinators like >, +, and the descendant space, add nothing. Neither does :where(), which is deliberately weightless.

To compare two selectors, you compare columns left to right. A single ID (1,0,0) beats any number of classes (0,50,0), because the first column is compared first and one beats zero. This is not base-10 arithmetic — 50 classes never “carry over” into an ID’s worth of weight. Think of it as comparing tuples, not adding up a single number.

Worked examples

A few selectors, scored:

Selectora (ID)b (class)c (type)Score
p001(0,0,1)
.intro010(0,1,0)
p.intro011(0,1,1)
#hero .intro a111(1,1,1)
#hero100(1,0,0)

Given #hero at (1,0,0) versus p.intro at (0,1,1), the ID selector wins, no matter that the second selector names two things. When two selectors tie exactly, source order breaks the tie: the rule that appears later in the stylesheet wins. This is why the order you load stylesheets — and the order of rules within them — matters.

Where !important and inline styles fit

Two things sit outside the normal specificity comparison.

Inline stylesstyle="color: red" written directly on the element — beat any selector in a stylesheet. In the four-column model they occupy the leftmost column that specificity can’t reach.

!important overrides everything, including inline styles, and jumps the rule into a separate, higher layer of the cascade. If two !important declarations conflict, specificity is compared among them. This is why !important wars escalate: once one rule uses it, the only way to beat that rule is another !important with equal-or-higher specificity. Reaching for it is usually a sign the underlying selectors need rethinking, not that you need a bigger hammer.

Why high specificity hurts

Specificity is not something to maximize. High-specificity selectors are hard to override, and overriding is the everyday work of maintaining a stylesheet — theming, responsive tweaks, component variants. A rule written as #sidebar ul li a.active scores (1,1,2) and forces every later override to match or exceed it, dragging the whole file’s baseline upward. Months later, someone adds !important just to change a link color, and the spiral begins.

The healthier pattern is to keep selectors flat and class-based. A single class like .nav-link--active scores (0,1,0) and is trivially easy to override with another single class. This is the core idea behind methodologies like BEM and behind utility-first frameworks such as Tailwind CSS, where nearly every selector is a single class of equal weight, so source order — not an escalating specificity ladder — decides the winner.

The modern tools that tame specificity

Recent additions to CSS give you explicit control instead of relying on selector tricks.

  • :where() wraps selectors but contributes zero specificity. :where(#hero) .intro scores only (0,1,0) — the ID inside is neutralized. It is ideal for library or reset styles that should be effortless to override.
  • :is() works like :where() but takes the specificity of its most specific argument, which is useful for grouping without losing weight entirely.
  • Cascade layers (@layer) let you declare priority bands explicitly. A rule in a later layer beats a rule in an earlier layer regardless of specificity, so you can put resets, frameworks, and your own components in a deliberate order and stop fighting the score.

These sit alongside other layout-era features like container queries, which shift decisions from global selectors to component context. The theme across all of them is the same: make intent explicit rather than encoding it in ever-longer selector chains.

How specificity relates to the rest of CSS

Specificity only decides which declaration applies. What that declaration then does still depends on the rest of the language — how values inherit down the tree, how the box model turns those values into sizes, and how layout systems like Grid and Flexbox arrange the result. If you are still building a mental model of how the pieces fit, our overview of what CSS is is a good companion, and understanding the DOM helps because selectors ultimately match against that tree.

When a style isn’t applying, the browser devtools are the fastest diagnosis: the Styles panel shows every rule targeting an element, strikes through the ones that lost, and tells you which selector won. Reading that panel with the scoring model in mind turns “why won’t this work” into a two-second answer.

The takeaway

CSS specificity scores selectors by counting IDs, then classes, then element types, and compares those counts column by column — a single ID outranks any pile of classes. Source order breaks exact ties; inline styles and !important sit above the normal comparison. The practical goal is not to win specificity battles but to avoid them: keep selectors flat and class-based, reach for :where(), :is(), and @layer when you need explicit control, and treat a spreading !important as a signal to lower your selectors rather than raise your weapons.

Takina Takina · · 4 min read

CSS object-fit and object-position, Explained

object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.

#CSS #Web Development #Frontend
Takina Takina · · 5 min read

CSS inherit, initial, unset & revert Explained

CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.

#CSS #Web Development #Frontend
Takina Takina · · 4 min read

Dynamic Viewport Units: dvh, svh, and lvh Explained

dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.

#CSS #Web Development #Frontend