understood and controlled on purpose
The CSS cascade is not a matter of chance, it is a precisely defined algorithm. Anyone who understands how the browser weighs origin, importance, specificity and order writes stylesheets that behave predictably, instead of fighting their own rules with !important.
Table of contents
- 1. What the CSS cascade actually is
- 2. The three origins: user-agent, author and user
- 3. Importance and !important
- 4. Specificity: the (a, b, c) model
- 5. Calculating specificity: practical examples
- 6. Order as the final tiebreaker
- 7. Inheritance vs. cascade
- 8. Cascade tiers compared
- 9. In practice: diagnosing cascade problems
- 10. Summary
- 11. FAQ
1. What the CSS cascade actually is
The CSS cascade is the algorithm a browser uses to decide which property value gets applied to an element when several declarations set the same property. The term "cascade" describes the multi-stage process that weighs origin, importance, specificity and, finally, source order, one after another. Only once all the previous stages are tied does the declaration that appears later in the source code win.
The misconception behind most CSS problems is this: developers treat the CSS cascade like a simple priority list, "an ID beats a class, a class beats a tag." In reality the cascade is a multi-dimensional algorithm that ranks origin and importance above specificity. An !important declaration from the user stylesheet beats every author declaration, no matter how high its specificity is. Knowing this stops you from writing ever more specific selectors and painting yourself into a corner.
The formal definition of the CSS cascade comes from the CSS Cascading and Inheritance Specification, currently at Level 5. Browsers implement this specification with only minimal deviations, and the behavior is consistent and testable across modern browsers, a huge improvement over the early days of CSS, when vendors had their own interpretations.
2. The three origins: user-agent, author and user
The CSS cascade distinguishes between three origins of stylesheets. The user-agent stylesheet is the browser's built-in stylesheet: it defines that <h1> appears large and bold, that <a> is blue and underlined, and that <ul> gets bullet points. This stylesheet is the foundation of every web page, even when no external CSS is loaded at all. Every browser ships slightly different user-agent stylesheets, which is exactly why CSS resets and Normalize.css exist.
The author stylesheet is the stylesheet the developer writes: all external CSS files, <style> blocks in the HTML and inline styles belong to the author origin. In the cascade, the author origin has higher priority than the user-agent stylesheet by default. The user stylesheet is the stylesheet an end user can define in their own browser, rarely used in modern browsers but technically still present. It sits between the user-agent and author origins in the default cascade, though in a web developer's daily practice it plays a minor role, except in combination with !important.
The interplay between origins explains why a CSS reset is necessary at all: the browser brings its own assumptions about spacing, font sizes and list rendering. A reset explicitly sets these values from scratch, so the author declarations build on a neutral base. all: revert is the modern way to do this: it resets a property to the value the user-agent stylesheet would have set for that element, which is useful for shadow DOM and web components.
/* Example: cascade origins in practice */
/* 1. User-Agent origin (built-in browser default, simplified) */
/* h1 { font-size: 2em; font-weight: bold; margin-block: 0.67em; } */
/* 2. Author origin: overrides User-Agent for all h1 elements */
h1 {
font-size: 1.875rem; /* 30px at root 16px */
font-weight: 700;
margin-block: 0;
line-height: 1.2;
}
/* 3. Inline style (still Author origin, highest specificity in Author) */
/* <h1 style="font-size: 2rem;">, beats the rule above */
/* Resetting to User-Agent value explicitly */
.widget h1 {
all: revert; /* hand control back to User-Agent for this context */
}
/* Resetting to initial (spec-defined) value, ignores User-Agent */
.reset-all {
all: initial;
}
3. Importance and !important
The importance of a declaration is the most powerful filter in the CSS cascade. A declaration marked with !important is pulled out of the normal cascade resolution and placed into a separate, higher-priority category. This is the crucial and often misunderstood part: !important reverses the origin priority. Normal author CSS beats normal user-agent CSS. But !important in user-agent CSS beats !important in author CSS. That is not a bug, it is a deliberate protection mechanism for accessibility.
In practice, !important in author CSS is a sign of unmanaged CSS cascade problems. Once a team starts overriding styles with !important, an arms race begins: the next person overrides that !important with an even more specific !important. The result is a stylesheet nobody can safely edit anymore. The clean fix is to address the root cause, either by lowering the specificity of the overriding selector or by reordering the declarations.
Still, legitimate use cases for !important in author CSS do exist: utility classes such as .hidden { display: none !important; } need to win every time, regardless of which component styles are active. Frameworks like Tailwind CSS deliberately use !important in their utility classes to guarantee that an explicitly applied helper class is never displaced by component styles. That is a sound approach as long as the entire CSS cascade context is well understood.
4. Specificity: the (a, b, c) model
Specificity is the second tier of the CSS cascade, and it only comes into play within the same origin and importance level. It is represented as a three-part tuple (a, b, c). The number a counts ID selectors (#header), b counts classes, attribute selectors and pseudo-classes (.nav, [type="text"], :hover), and c counts element selectors and pseudo-elements (div, ::before). The universal selector *, combinators (+, ~, >) and the negation pseudo-class :not() itself contribute no specificity at all, only their arguments do.
When comparing two specificities, the tuple with the higher a value always wins, no matter what b and c are. Only once a is equal does b decide, and then c. That means a single ID selector #nav (1,0,0) beats a selector chaining ten classes, .a.b.c.d.e.f.g.h.i.j (0,10,0). This is the most common misunderstanding in day-to-day CSS cascade work: developers stack classes because they believe enough classes can eventually override an ID, which simply does not work.
5. Calculating specificity: practical examples
Calculating specificity is mechanical, and it becomes intuitive after a bit of practice. Knowing exactly what specificity a given selector carries is the prerequisite for writing CSS that does not run into unmanaged CSS cascade conflicts. Selectors that look low-specificity at first glance but reach a high specificity through combination are particularly tricky, for example nav ul li a:hover (0,1,4), which cannot be overridden by a single class (0,1,0).
Modern pseudo-classes like :is(), :has() and :not() take on the specificity of their most specific argument. :is(#nav, .menu) has specificity (1,0,0) because #nav is the most specific argument, even if the element only actually matches through .menu. That matters when you introduce these modern selectors into an existing CSS cascade architecture. :where(), by contrast, always has specificity (0,0,0): it makes a selector list usable without contributing any specificity, which is ideal for base styles that are meant to be easily overridden.
/* Specificity calculation examples */
/* (0, 0, 1): one element */
p { color: gray; }
/* (0, 1, 0): one class */
.intro { color: blue; }
/* (0, 1, 1): one class + one element */
p.intro { color: navy; }
/* (1, 0, 0): one ID, beats all above */
#hero { color: black; }
/* (0, 1, 4): nav ul li a:hover, class wins over 4 elements */
nav ul li a:hover { color: violet; }
/* :is() takes specificity of its MOST specific argument */
/* (1, 0, 0): because #nav is the most specific in the list */
:is(#nav, .menu) a { color: purple; }
/* :where() always contributes (0, 0, 0), stays overridable */
:where(header, main, footer) p { margin-block: 1em; }
/* Inline style = (1, 0, 0, 0), separate layer above IDs */
/* <p style="color: red;">, beats #hero above */
/* !important in Author CSS, floats above normal cascade */
.utility-hidden { display: none !important; }
6. Order as the final tiebreaker
When two declarations tie on origin, importance and specificity, source order decides: the later declaration wins. This final criterion in the CSS cascade is intuitive and predictable. CSS files loaded later in the HTML override earlier ones. Within a single file, later rules override earlier rules of equal weight.
Order also plays a decisive role when loading third-party CSS. If you load Bootstrap or another library before your own stylesheet, you automatically get the overriding advantage, all your own rules of equal specificity win. Flip the order and load the library last, and the library's styles win in a tie. That is a common and hard-to-diagnose CSS cascade bug: the stylesheet appears broken simply because the load order got swapped.
The same logic applies inside @media blocks: a @media block does not raise specificity, it only filters whether the rule applies at all. Two @media (min-width: 768px) blocks carry equal priority, and the one later in the source code wins. That is exactly why, in a mobile-first approach, breakpoints must be written in ascending order.
7. Inheritance vs. cascade
Inheritance and the CSS cascade are two separate mechanisms that get confused often. The cascade resolves conflicts between declarations that target the same element and the same property. Inheritance is a different mechanism entirely: when no selector sets a property for an element, certain properties can inherit their value from the parent element. Not every property is inherited, color and font-family are, margin and border are not.
The explicit cascade keywords give you precise control: inherit forces inheritance from the parent element, even for properties that normally do not inherit. initial sets the value defined as the initial value in the CSS specification. unset behaves like inherit for inheritable properties and like initial for non-inheritable ones. revert resets to the value the user-agent stylesheet would have applied to that element. These keywords are powerful tools for jumping back to a specific tier of the CSS cascade on purpose, without having to hard-code specific values.
8. Cascade tiers compared
The full resolution order of the CSS cascade is more precise than the simplified version many introductions give. Browsers work through the following tiers in exactly this order.
| Priority | Origin & importance | Example | Practical relevance |
|---|---|---|---|
| 1 (highest) | Transition declarations | transition: color 0.3s |
While the transition is active |
| 2 | !important user-agent | Browser accessibility styles | Cannot be overridden by author |
| 3 | !important user | User stylesheet with !important | Accessibility override |
| 4 | !important author | .hidden { display: none !important } |
Utility classes, use sparingly |
| 5 | Normal author | All regular stylesheet rules | Main working area |
| 6 | Normal user | User stylesheet without !important | Rare in the modern web |
| 7 (lowest) | Normal user-agent | Browser defaults for HTML tags | Baseline without any custom CSS |
This table makes clear why !important is so powerful: it lifts a declaration out of the normal author tier (priority 5) up to priority 4, which overrides every normal author declaration regardless of its specificity. At the same time, the table shows that !important in the user-agent (priority 2) and user stylesheet (priority 3) is even more powerful, a deliberate design that protects a user's accessibility settings from being overridden by author styles.
9. In practice: diagnosing cascade problems
The most effective tool for diagnosing CSS cascade problems is the browser's DevTools. In Chrome and Firefox, the Styles panel in the inspector lists every active and overridden declaration for an element, with overridden rules shown with a strikethrough. Since Chrome 121, specificity is displayed directly next to each selector as a tooltip. That makes it possible to spot within seconds which selector is displacing a rule you expected to apply.
A systematic diagnostic workflow looks like this: first check whether the property is being applied at all (explicitly set, not just inherited). Then check the origin, does the overriding rule come from an external framework? Next, compare the specificity of the competing selectors. Only as the very last step should you check the order in the source code. Anyone who follows this workflow finds CSS cascade conflicts without blindly stacking classes or reaching for !important.
One practical tip for large projects: the @layer directive (CSS Cascade Layers) lets you structure the entire CSS cascade stack explicitly, so that the load order of files no longer determines priority. That is a significant advantage over the classic specificity arms race, especially when integrating third-party CSS.
/* Diagnosing and fixing cascade conflicts without !important */
/* Problem: third-party library sets high-specificity rule */
/* .library .component .title { color: #333; }, (0, 3, 0) */
/* Wrong fix: adding !important */
/* .page-title { color: purple !important; } */
/* Right fix option 1: match or raise specificity */
.page .content .page-title { color: purple; } /* (0, 3, 0), same, wins by order */
/* Right fix option 2: use :is() to raise without nesting */
:is(.page, .landing) .page-title { color: purple; } /* (0, 2, 0), may still lose */
/* Right fix option 3: use an ID if contextually appropriate */
#main-content .page-title { color: purple; } /* (1, 1, 0), beats library */
/* Right fix option 4: use @layer (CSS Cascade Layers) */
@layer base {
.page-title { color: purple; } /* Wins over unlayered library if structured right */
}
/* Utility: revert to user-agent for isolated widgets */
.isolated-widget {
all: revert-layer; /* CSS Cascade Level 5, revert within layer context */
}
Mironsoft
CSS architecture, frontend development and Hyva theme specialization
Need to resolve CSS cascade conflicts cleanly?
We analyze CSS architectures, identify specificity problems, and structure stylesheets with cascade layers, for predictable, maintainable CSS without !important patchwork.
CSS audit
Specificity analysis, an !important inventory, and cascade conflict diagnosis
Refactoring
Introducing cascade layers, simplifying selectors, boosting maintainability
Training
Team workshops on the CSS cascade, specificity, and modern CSS architectures
10. Summary
The CSS cascade is a multi-stage algorithm: browsers check origin and importance first, then specificity, then order. Anyone who understands this process writes CSS that works without an !important escalation. The three origins, user-agent, author and user, have different default priorities, and those can be reversed with !important. Specificity is not a simple point system, it is a three-dimensional tuple (a, b, c) compared lexicographically.
The most important practical takeaways: avoid IDs in CSS to keep specificity overhead low. Use :where() for base styles that should stay easily overridable. Use :is() and :has() deliberately, since they take on the specificity of their most specific argument. Design the load order of CSS files intentionally. And when cascade conflicts arise with third-party CSS, reach for the new CSS cascade layers (@layer) as a structural solution instead of raising specificity.
The CSS cascade: the essentials at a glance
Resolution order
Origin plus importance, then specificity, then order. Each tier is only checked once the previous one ties.
!important, used deliberately
Reserve it for utilities like .hidden. It reverses origin priorities. In the user-agent stylesheet it always beats author !important.
Specificity (a, b, c)
IDs (a), classes/attributes/pseudo-classes (b), elements/pseudo-elements (c). :where() contributes (0,0,0).
Modern tools
@layer for structured cascade control. all: revert for a context reset. DevTools for fast diagnosis.