The question the CSS cascade answers
Rules come from three origins: the browser's default stylesheet, the user's settings, and the author, meaning you and every library you load. Several may set color on the same paragraph, and the cascade resolves that to one value. The name describes the shape: declarations fall through a series of tie-breakers until one is left.
The order of the tie-breakers is what an interviewer wants, because each explains a different family of bugs. In CSS Cascading Level 5 a later step is consulted only when the earlier ones tie:
- Origin and importance. Normal author styles beat normal user styles, which beat the browser's defaults.
- Context. Encapsulation, so styles inside a shadow tree compete with outer styles under their own rules.
- Element-attached styles. A
style attribute beats ordinary rules.
- Cascade layers. For normal declarations, a later
@layer beats an earlier one, and unlayered styles beat every layer.
- Specificity. The more specific selector wins.
- Order of appearance. The last declaration in source order wins.
Most developers learn the CSS cascade as "specificity, then last one wins", which is steps five and six. The surprising bugs live above them.
Specificity is three columns, not a score
CSS specificity is a triple: ID selectors, then classes, attributes and pseudo-classes, then type selectors and pseudo-elements. It is compared column by column, so one ID beats any number of classes. It is not a base-ten number, and eleven classes do not overflow into an ID.
Modern selectors change the arithmetic. :where() always contributes zero, which makes it the right tool for defaults that should be easy to override. :is(), :not() and :has() take the specificity of their most specific argument, so :is(#nav, a) weighs as much as an ID even when it matches the a.
Layers settle fights that specificity cannot
Cascade layers, supported in all major engines since 2022, sit above CSS specificity. That means a low-specificity rule in a later layer beats a high-specificity rule in an earlier one, and you can finally import a third-party stylesheet without entering a specificity contest with it.
@layer reset, vendor, components;
@import url("datepicker.css") layer(vendor);
@layer components {
.picker .day { color: navy; }
}
.day { color: crimson; }
The last rule surprises people: unlayered author styles outrank all layered ones for normal declarations, so adding layers to half a codebase silently demotes the half you layered.
How !important turns the order upside down
!important does not add weight to a declaration. It moves the declaration into a separate importance band where the precedence is reversed. Important user styles beat important author styles, and important browser defaults beat both, which is how accessibility settings in a user stylesheet are meant to hold against a site. The same inversion applies to layers: an !important in your earliest layer beats an !important in your latest, and important layered declarations beat important unlayered ones.
A team that reaches for !important to win a fight has not escalated; it has moved the fight to a band where the rules run backwards.
Inheritance is not a weak cascade rule
The trap that separates a strong answer is treating inheritance as part of the CSS cascade. It is not. An inherited value is used only when the cascade found no declaration at all for that property on that element. It never enters a specificity contest at all.
That explains a classic bug. You set #app { color: navy }, the links stay blue, and raising the selector to three IDs changes nothing. The browser's default stylesheet declares a colour for links directly, and any declaration on the element, even a browser default, beats a value inherited from the parent. It is also why buttons and inputs ignore your body font in most browsers: the default stylesheet sets their font directly. The fix is a rule on the element itself, such as a { color: inherit } or button { font: inherit }, not a heavier rule on an ancestor.
The bug that only appears in production
Order of appearance is the step people dismiss, and it is behind styles that change between environments. Two rules with equal specificity in the same layer are decided by which stylesheet loaded last. In development the bundler may inject them in one order; in production, after code splitting, the chunk for one route arrives before the other on some pages and after it on others. The CSS did not change; the sequence did.
The diagnosis is always the same: in the browser's styles panel, find the winning declaration, read the struck-through ones beneath it, and ask which cascade step separated them. If it was source order, putting each source in a named layer makes the result independent of load order.
The CSS cascade ranks declarations on one element, and inheritance only fills the gaps it leaves. When a rule "does not apply", find the declaration that beat it and name the step that decided it, rather than adding specificity or !important and hoping.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).