Two rules that set the same property on the same element will conflict, and something has to decide which one applies. That decision is the cascade: a defined order of origins, a score for each selector called specificity, and a final tie-break on order. Once you know the three, the "why is my colour wrong" questions disappear.
What is the order the browser applies?
The cascade compares declarations in this order, and the first step that separates them decides the winner.
1. origin and importance
user-agent < user < author < animations < !important author < !important user
2. cascade layers (later layers win over earlier ones)
3. specificity (higher wins)
4. source order (later wins)
Almost everything you write is "author" style, which is why an inline style and a stylesheet rule both compete on the next steps. The one place user styles appear is the browser's own rules — the blue default link colour, the default border on a field — and a user who has customised their browser gets their choice over yours, which is deliberate.
Where the modern part comes in is cascade layers. Declaring a layer lets you say "these rules always beat those rules" without writing !important or inflating specificity.
@layer reset, base, components, utilities;
@layer components {
.button { background: #800d4d; }
}
@layer utilities {
.button--ghost { background: transparent; }
}
Within the layer, the normal rules still apply. Between layers, a later layer wins no matter how specific its selector is. That is exactly the escape hatch a design system needs: a utility class in a later layer can override a component class without a specificity war.
How is specificity calculated?
Each selector gets a score with three parts, and they are compared like a version number: IDs first, then classes and attributes and pseudo-classes, then element names and pseudo-elements.
selector score example
p 0-0-1 element
.card 0-1-0 class
.card p 0-1-1 class + element
p:first-child 0-1-1 pseudo-class counts as a class
.card:hover 0-2-0 two classes
#featured 1-0-0 id
#featured .card 1-1-0 id + class
Two details are counter-intuitive the first time. A pseudo-class such as :hover counts as a class, not as an element. And a pseudo-element such as ::before counts as an element, so p::before is 0-0-2.
The :where() selector is the escape hatch: it always scores zero, whatever is inside it. :is() takes the score of its most specific argument, so it behaves like the most demanding selector in the list.
:where(a, button) { color: inherit; } /* 0-0-0 */
:is(.btn, .link) { padding: 0.5rem 1rem; } /* 0-1-0 */
Why is my rule not applying?
There is a short list of reasons, in the order they are worth checking. The selector may not match the element at all — check that first, because it is the most common cause and the least obvious. The rule may be overridden by a later rule with the same specificity. A more specific selector may win. An inline style may win, because inline styles beat any selector. A !important somewhere may win.
<style>
.card { color: #222; } /* 0-1-0 */
#featured { color: #800d4d; } /* 1-0-0 — wins, and no amount of editing the class rule beats it */
</style>
<div class="card" id="featured">...</div>
The fix is almost never !important. The fix is to use the same specificity as the rule you are fighting and put yours later, or to restructure so the question does not arise — which is what layers are for.
What about inline styles and !important?
An inline style is a declaration in the element's own style attribute, and it beats any selector-based rule. It is the reason a one-off inline value cannot be overridden by a stylesheet, which is why inline styles are a last resort for styling.
!important beats everything except another !important, and between two of those the later one wins. It is a genuine escape hatch for a few situations — overriding a third-party stylesheet you do not control, or a utility that must win against a component. It is not a way to win an argument with your own stylesheet.
/* legitimate: your own base styles, and one utility that must win */
.card { color: #222; }
.text-danger { color: #b3261e !important; }
How do you find out which rule won?
When a style does not apply, the reason is almost always one of four things, and you can check them in order without guessing.
- The rule does not match the element at all. A typo in a class name, or a selector that expects a descendant where there is none.
- Another rule later in the file wins on specificity.
- An inline style or an
!importantdeclaration overrides everything. - The property is inherited rather than set, so the element is showing its parent's value.
The browser can tell you which of these it is. Open the Elements panel, select the element, and look at the Styles pane. Every rule that applies is listed in order, with the winning one highlighted, and any rule that was overridden is shown greyed out with a strikethrough. Rules that did not match at all are not shown, which is itself the answer - if the rule you expected is missing from the list, the selector never matched.
The Computed pane below it gives you the final value of every property, whatever the route to that value happened to be. When the Styles pane and the Computed pane disagree, the Computed pane is the truth: it is what the browser actually used.
<!-- check the resolved value of any property straight from the console -->
getComputedStyle(document.querySelector('.menu')).color
<!-- and the height the element actually took -->
document.querySelector('.menu').getBoundingClientRect().heightOne habit to adopt from that panel: when a rule loses, the panel tells you which rule beat it. Read that line, and you have the answer. Chasing the cascade by memory is what makes people reach for !important - and one !important is far more expensive to live with than five minutes of reading the Styles pane.
Quick reference
- The cascade compares origin and importance, then layers, then specificity, then source order.
- Specificity is three numbers: IDs, then classes, then elements — compared left to right.
- A pseudo-class counts as a class; a pseudo-element counts as an element.
:where()scores zero;:is()scores its most specific argument.- Cascade layers let later layers win regardless of specificity — the clean way to win.
- An inline style beats any selector;
!importantbeats everything but another!important.
Try it yourself: write two rules that conflict, one with a class and one with an id, and predict the winner before reloading. Then rewrite the id rule as a class that comes later and confirm the result is the same colour for a different reason. Finally, put one rule in a later cascade layer and watch the layer order take over from specificity entirely.