0-0-0-0 Notation, ID vs. Class, !important
CSS specificity is the most common source of inexplicable styling conflicts. Why doesn't a rule take effect even though it appears further down in the stylesheet? Why does a short selector override a rule that seems to match more precisely? The answer lies in CSS specificity, a calculation system that underpins every piece of stylesheet work.
Table of Contents
- 1. What CSS specificity really is
- 2. The 0-0-0-0 notation explained
- 3. ID, class, type: the three weight classes
- 4. Inline styles: the strongest source
- 5. !important: when it makes sense
- 6. The cascade: more than just specificity
- 7. Pseudo-classes and pseudo-elements
- 8. Specificity head to head
- 9. Strategies for controlling specificity
- 10. Summary
- 11. FAQ
1. What CSS specificity really is
When several CSS rules define the same property on an element, the browser uses a ranking system to decide which rule wins. This ranking system is called CSS specificity. It is not an arbitrary mechanism but a mathematically precise algorithm defined in the CSS specification. Anyone who understands CSS specificity can predict which rule applies, without guessing, without trial and error, and without reaching for !important as a universal weapon.
The misunderstanding is common: many developers believe that the order of rules in the stylesheet decides which one applies. That is only partly true. Source order is the last criterion applied, and only when everything else is equal. Before that comes CSS specificity, and before that the origin criterion of the cascade (browser default, user stylesheet, author stylesheet). Anyone debugging a rule needs to work through this hierarchy in the correct order, otherwise they end up looking in the wrong place.
2. The 0-0-0-0 notation explained
CSS specificity is expressed as four digits: a-b-c-d, where each digit represents a selector category. Digit a stands for inline styles: a style attribute directly in the HTML gets the value 1, everything else gets 0. Digit b counts ID selectors (#id). Digit c counts class selectors (.class), attribute selectors ([attr]) and pseudo-classes (:hover, :focus). Digit d counts type selectors (h1, div, span) and pseudo-elements (::before, ::after).
When comparing two selectors, the comparison runs from left to right: first a, then b, then c, then d. As soon as one digit is higher, that selector wins, regardless of the remaining digits. A selector with 0-1-0-0 (one ID) always beats a selector with 0-0-99-0 (99 classes), because the b digit is decisive. That is the core of the CSS specificity system: there is no addition across category boundaries. IDs and classes belong to different categories and can never offset each other.
/* CSS Specificity: 0-b-c-d calculation examples */
/* 0-0-0-1: type selector */
p { color: gray; }
/* 0-0-0-2: two type selectors */
article p { color: #374151; }
/* 0-0-1-0: one class selector */
.intro { color: #4b5563; }
/* 0-0-1-1: one class plus one type selector */
p.intro { color: #1e1b4b; }
/* 0-0-2-1: two classes plus one type */
.card .intro { color: #4a1d96; }
/* 0-1-0-0: one ID selector, beats ALL of the above */
#main { color: #7c3aed; }
/* 0-1-1-1: ID plus class plus type */
#main .intro p { color: #6d28d9; }
/* 1-0-0-0: inline style beats all selector-based rules */
/* <p style="color: red"> ... </p> */
/* Specificity is NOT additive across categories: */
/* 0-0-10-0 (ten classes) is less than 0-1-0-0 (one ID) */
/* This counterintuitive fact causes many debugging headaches */
3. ID, class, type: the three weight classes
The three selector categories in CSS specificity carry different weights, and the relationship between them is absolute, not relative. ID selectors (#nav, #header) have the highest specificity among selectors. A single ID always beats any chain of class and type selectors, no matter how long. That makes IDs problematic in stylesheets: they create specificity barriers that are hard to override without resorting to another ID or !important.
Class selectors, attribute selectors and pseudo-classes all belong to the same category with the same weight. .card, [type="text"] and :hover each count as 0-0-1-0. Type selectors and pseudo-elements have the lowest weight: div, p, span, ::before, ::after each count as 0-0-0-1. The universal selector * and combinators (>, +, ~, whitespace) contribute nothing to CSS specificity; their number plays no role in the calculation.
/* Specificity calculations, broken down step by step */
/* a=0, b=0, c=0, d=1 -> 0-0-0-1 */
h2 { }
/* a=0, b=0, c=0, d=2 -> 0-0-0-2 (two type selectors) */
main h2 { }
/* a=0, b=0, c=1, d=0 -> 0-0-1-0 (one class) */
.heading { }
/* a=0, b=0, c=1, d=1 -> 0-0-1-1 (class plus type) */
h2.heading { }
/* a=0, b=0, c=2, d=1 -> 0-0-2-1 (two classes plus type) */
.section .heading { }
/* Note: the descendant combinator (space) adds 0 */
/* a=0, b=0, c=0, d=0 -> 0-0-0-0: universal selector */
* { box-sizing: border-box; }
/* a=0, b=0, c=1, d=0 -> 0-0-1-0: attribute selector, same as class */
[lang="en"] { }
/* a=0, b=0, c=1, d=0 -> 0-0-1-0: pseudo-class, same as class */
:hover { }
/* a=0, b=0, c=0, d=1 -> 0-0-0-1: pseudo-element, same as type */
::before { }
/* a=0, b=1, c=0, d=0 -> 0-1-0-0: ID selector */
#main-content { }
/* This beats 0-0-99-0: no number of classes overrides one ID */
4. Inline styles: the strongest source
Inline styles, meaning CSS declarations in the style attribute of an HTML element, have the highest CSS specificity among regular styles: 1-0-0-0. No selector in an external or embedded stylesheet can override an inline style, except with !important. This matters often in the context of JavaScript-based frameworks and UI libraries: libraries frequently set inline styles for dynamic values such as positions, transforms or colors.
In practice, inline styles in static HTML documents should be avoided where possible: they break the stylesheet maintenance chain and can only be overridden with !important. In dynamic applications they are often unavoidable, for example for JavaScript-computed animation values. A pattern that works well: set inline styles for dynamic values as CSS custom properties, for example element.style.setProperty('--offset', px + 'px'), and then use those custom properties within the stylesheet cascade. That keeps CSS specificity controllable at the stylesheet level.
5. !important: when it makes sense
The !important annotation is not, strictly speaking, part of CSS specificity. It belongs to a separate cascade layer that overrides all specificity-based rules. A declaration with !important wins against any declaration without !important, regardless of specificity. When several !important declarations compete, normal specificity applies between them again.
The important question is: when is !important legitimate? It is legitimate for utility classes that should never be overridden. The Tailwind CSS concept of utility-first relies precisely on this pattern. Utility classes like .hidden or .sr-only with !important ensure that the intended behavior cannot be undermined by component styles. It is not legitimate as a debugging aid that stays permanently in the code. Every !important left in place because the actual specificity conflict was never resolved is technical debt.
/* !important: legitimate and illegitimate uses */
/* LEGITIMATE: utility classes that must never be overridden */
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
}
.hidden {
display: none !important;
}
/* LEGITIMATE: user preference overrides (e.g., high contrast mode) */
@media (forced-colors: active) {
.badge { color: ButtonText !important; background: ButtonFace !important; }
}
/* NOT LEGITIMATE: !important as a specificity conflict band-aid */
/* This means you have an unresolved specificity problem */
.product-title {
color: #1e1b4b !important; /* Should fix the selector instead */
}
/* BETTER: resolve the conflict properly */
/* If #product-page .title is beating .product-title, use: */
.product-page .product-title { /* 0-0-2-0 beats 0-1-0-1, no! */
color: #1e1b4b;
}
/* Or reduce ID usage: use a class instead of #product-page */
.product-page-layout .product-title { color: #1e1b4b; } /* 0-0-2-0 */
6. The cascade: more than just specificity
The full cascade hierarchy in CSS is more layered than pure CSS specificity. The priority levels from highest to lowest are: transition declarations, browser animations and keyframes, important declarations from user-agent stylesheets (!important in the browser default), important user declarations, important author declarations, normal keyframes, normal author declarations (this is where regular stylesheet code lives), normal user declarations, and normal user-agent declarations (browser defaults). Within normal author declarations, CSS specificity then comes into play.
CSS Cascade Layers (@layer) add another hierarchy level within author declarations. With @layer base, components, utilities you define an order that complements specificity: rules in later layers win against rules in earlier layers when CSS specificity is equal. This enables design systems with an explicit override hierarchy, without !important inflation and without artificially inflating specificity.
7. Pseudo-classes and pseudo-elements
Pseudo-classes and pseudo-elements carry different specificity weights. Pseudo-classes such as :hover, :focus, :first-child, :nth-child(), :not(), :is(), :has() count in the c column of CSS specificity, the same weight as class selectors. Pseudo-elements such as ::before, ::after, ::first-line, ::placeholder count in the d column, the same weight as type selectors.
The pseudo-class :not() is a special case: it contributes nothing to CSS specificity on its own, but its argument is counted. :not(.active) has a specificity of 0-0-1-0, since the class in the argument counts. The same applies to :is() and :has(): they count the specificity of their most specific argument. :where(), on the other hand, always has zero specificity: neither the pseudo-class itself nor its arguments contribute to CSS specificity. That makes :where() the tool of choice for deliberately low-specificity rules.
8. Specificity head to head
Many CSS debugging situations come down to a direct comparison of two selectors. The comparison scheme below helps you analyze conflicts quickly. The rule of thumb always holds: check the origin first (inline vs. stylesheet), then CSS specificity, then source order.
| Selector A | Selector B | Specificity A | Specificity B | Winner |
|---|---|---|---|---|
.card h2 |
h2 |
0-0-1-1 | 0-0-0-1 | A wins |
#header |
.header.top.sticky |
0-1-0-0 | 0-0-3-0 | A wins (ID) |
.nav a:hover |
.nav-link:hover |
0-0-2-1 | 0-0-2-0 | A wins (type) |
p |
:where(p) |
0-0-0-1 | 0-0-0-0 | A wins (:where = null) |
style="color:red" |
#main h1 |
1-0-0-0 | 0-1-0-1 | A wins (inline) |
A practical debugging strategy: browser DevTools show a struck-through rule exactly when it has been overridden by a more specific one. In Chrome DevTools, the Computed Styles panel shows a specificity value right next to each selector. Anyone who needs to calculate CSS specificity conflicts manually can use online tools such as specificity.keegan.st, which accept a CSS selector and return its numeric specificity.
9. Strategies for controlling specificity
The three most effective strategies for controlling CSS specificity are: keep IDs out of stylesheets, use :where() for base styles, and use @layer for explicit override hierarchies. Using IDs in a stylesheet is an anti-pattern in almost every case: they create specificity barriers that are hard to override. IDs are useful for JavaScript anchors and accessibility attributes, but #hero as a styling selector in a stylesheet is almost never a good idea. Classes offer the same semantics and can be combined and overridden freely.
CSS @layer has been available since Chrome 99, Safari 15.4 and Firefox 97, and it enables explicit override hierarchies without CSS specificity inflation. With @layer base, components, utilities, rules in the utilities layer win against equally specific rules in components and base. That makes Tailwind-style utility systems possible without !important. :where() and @layer together let you design the entire CSS specificity of a design system deliberately, instead of letting it emerge by accident.
/* Specificity control strategies */
/* Strategy 1: @layer for explicit override hierarchy */
@layer base, components, utilities;
@layer base {
/* Low specificity reset, easily overridden by components */
:where(h1, h2, h3, h4) {
font-weight: 700;
line-height: 1.25;
}
}
@layer components {
/* Component styles, override base automatically due to layer order */
.card-title {
font-size: 1.25rem;
color: #1e1b4b;
}
}
@layer utilities {
/* Utilities always win, last layer, no !important needed */
.text-violet { color: #7c3aed; }
.font-bold { font-weight: 700; }
}
/* Strategy 2: avoid IDs in stylesheets, use data attributes if needed */
/* BAD: */ #product-hero { background: #ede9fe; }
/* GOOD: */ .product-hero { background: #ede9fe; }
/* GOOD: */ [data-section="hero"] { background: #ede9fe; }
/* Note: attribute selectors have class-level specificity (0-0-1-0) */
/* Strategy 3: :where() for base styles, zero specificity */
:where(article, .content) :where(p, li) {
line-height: 1.7;
color: #374151;
/* Specificity: 0-0-0-0, any later rule overrides this */
}
10. Summary
CSS specificity is a mathematically precise system expressed as the 0-0-0-0 notation: inline styles (a), ID selectors (b), classes, attributes and pseudo-classes (c), types and pseudo-elements (d). Comparison runs from left to right, and the first unequal digit decides. One ID always beats any number of classes. Inline styles beat all selectors. !important overrides everything, but it should only be used for legitimate utility classes and user overrides.
Modern CSS tools let you shape CSS specificity deliberately instead of fighting it reactively: keep IDs out of stylesheets, use :where() for zero-specificity base styles, and use @layer for explicit override hierarchies. Anyone who applies these strategies consistently builds CSS architectures that stay maintainable even in large projects, without specificity wars, without !important inflation, and without the feeling of fighting their own stylesheet.
CSS Specificity: the essentials at a glance
0-0-0-0 Notation
a=inline, b=ID, c=class/attribute/pseudo-class, d=type/pseudo-element. Compared left to right. The first unequal digit decides, with no addition across categories.
Avoid IDs
#id in stylesheets produces 0-1-0-0, overriding anything built from classes. Use classes (.class) or data attributes instead.
!important, done right
Legitimate for utility classes (.hidden, .sr-only) and user preferences. Not as a band-aid for unresolved specificity conflicts.
Modern strategies
:where() for zero-specificity base styles. @layer for override hierarchies without !important. Plan CSS specificity instead of reacting to it.
Mironsoft
Modern CSS, Hyva themes and maintainable CSS architectures
CSS conflicts that just won't resolve?
We analyze existing CSS architectures, identify specificity conflicts, and modernize stylesheets with @layer, :where() and deliberate specificity management, for Hyva and Magento projects that stay maintainable.
Specificity audit
Analysis of !important inflation, ID misuse and cascade conflicts in existing stylesheets
@layer migration
Introducing CSS Cascade Layers and replacing !important rules with clean layer hierarchies
CSS architecture
Design system foundations with :where() and deliberate specificity management for scalable projects