A few weeks back I got to start a short series on CSS fundamentals. If you’re in the front-end web development space, CSS is one of those key things to know. Whether you’re into CSS-in-JS or you’d rather just have plain ol’ CSS, knowing how CSS works under the hood is crucial for writing efficient, scalable CSS.
The first post in this series was a deep dive into how the browser actually renders CSS to pixels. In this second post, we’ll dive into an often-misunderstood feature of the CSS language — the cascade.
The cascade is inherent to working with CSS — after all, it is what gives “Cascading Style Sheets” their cascading nature. The cascade can be a powerful tool, but using it wrong can lead to brittle stylesheets that give front-end developers nightmares any time they have to make a change. As we dive into the cascade, we’ll also look at a few ways to keep the cascade from getting out of hand.
Since we’ll be talking about the specifics of how the CSS Cascade works, it’ll be helpful for us all to be on the same page.
Here’s the definition from the CSS Cascade Level 4 Spec.
The cascade takes a unordered list of declared values for a given property on a given element, sorts them by their declaration’s precedence, and outputs a single cascaded value.
The CSS Cascade is the algorithm by which the browser decides which CSS styles to apply to an element — a lot of people like to think of this as the style that “wins”.
To understand the CSS cascade better, it’s helpful to think of a CSS declaration as having “attributes”. These attributes could be various parts of the declaration — like the selector or the CSS properties — or they can be related of where the CSS declaration exists (like it’s origin or the position in the source code).
The CSS cascade takes a few of these attributes and assigns each of them a weight. If a CSS rule wins at a higher-priority level, that’s the rule that gets wins.
However, if there are two rules still in conflict at a given weight, the algorithm will continue to “cascade down” and check the lower-priority attributes until it finds one that wins.
Here are the attributes that the CSS Cascade algorithm checks, listed in order from highest weight to least weight.
Don’t worry, we’ll get into each of these in-depth.
The highest weighted attribute that the cascade checks is a combination of the importance and the origin of a given rule.
As far as the origin of a CSS rule goes, there are three places that a rule can come from.
The importance of a CSS declaration is determined by the appropriately-named !important
syntax. Adding !important
to a CSS rule automatically jumps it to the front of the cascade algorithm, which is why it’s often discouraged. Overriding styles that use !important
can only be done with other rules that use !important
, which over time can make your CSS more brittle. Many people (myself included) recommend that you only use !important
as an escape hatch for when all else fails (such as when working with 3rd-party styles).
The cascade algorithm considers the combination of these 2 attributes when figuring out which declaration wins. Each combination is given a weight (similar to the way parts of a CSS declaration are weighted), and the declaration with the highest weight wins. Here are the various combinations of origin & importance that the browser considers, listed in order from highest weight to least weight.
!important
!important
!important
@keyframes
(This is the only exception, it is still originating from the author, but as animations are temporary/fleeting the browser weights them slightly higher than normal author rules)When the browser comes up against 2 (or more) conflicting CSS declarations and one wins at the origin & importance level, the CSS cascade resolves to that rule. No questions asked. Game over.
However, if the conflicting declarations have the same level of importance/origin, the cascade moves on to consider selector specificity.
The second weight in the CSS cascade is selector specificity. In this tier, the browser looks at the selectors used in the CSS declaration.
As a front-end developer, you only have control over the “author” origin stylesheets on your websites — you can’t do much to change the origin of a rule. However, if you’re staying away from using !important
in your code, you’ll find that you have a lot of control over the cascade at the specificity tier.
Similar to the way that the combinations of origin & importance each have their own weight, different types of CSS selectors are assigned priority. When evaluating specificity, the number of selectors and their priority are considered. CSS selectors can belong to one of the following weighted tiers.
style
tag)h1
) & pseudo-elements (::before
)If you have 2 CSS declarations with the same number of high-priority selectors, the resolution algorithm will consider the number of selectors at the next level of specificity. For example, if both of these CSS rules were targeting the same element, the color would be red. This is because they both have 1 id
selector, but the second rule has 2 class
selectors.
#first .blue h1 { color: blue; } #second .red.bold h1 { color: red; }
Many people like to manage specificity by simply not relying on it. Keeping your selector specificity low makes sure that your CSS rules stay flexible.
In my experience, if you default to only using class
selectors for your custom styles and element
selectors for your default styles, it’s way easier to override styles when you actually need to. If your CSS declarations have very high selector specificity you find yourself resorting to !important
more and that can get ugly pretty quickly.
The last main tier of the CSS cascade algorithm is resolution by source order. When two selectors have the same specificity, the declaration that comes last in the source code wins.
Since CSS considers source order in the cascade, the order in which you load your stylesheets actually matters. If you’ve got 2 stylesheets linked in the head of your HTML document, the second stylesheet will override rules in the first stylesheet. This is also the reason that if you’re using a CSS reset or a CSS framework, you’ll want to load that before your custom styles.
While initial & inherited values aren’t truly part of the CSS cascade, they do determine what happens if there are no CSS declarations targeting the element. In this way, they determine default values for an element.
Inherited properties will trickle down from parent elements to child elements. For example, the font-family
& color
properties are inherited. This behavior is what most people think of when they see the word “cascade” because styles will trickle down to their children.
In the following example, the <p>
tag will render with a monospace font & red text, since its parent node contains those styles.
<div style="font-family: monospace; color: red;">
<p>inheritance can be super useful!</p>
</div>
For non-inherited properties, each element has a set of initial values — these values are defined in the CSS spec for any given rule. For example, the initial value for the background-color
property is transparent
. If no CSS declaration sets a value for background-color
on an element, it will default to transparent
.
In addition, you can explicitly opt to use inherited or initial values in a CSS declaration by using the inherit
or initial
keywords in your CSS rule.
div { background-color: initial; color: inherit; }
Since the CSS cascade is one of the more misunderstood parts of CSS (and often the source of a lot of bugs), knowing how it works will give you a huge edge on keeping your stylesheets maintainable.
Knowing how to leverage CSS selector specificity to your advantage is a huge skill — I’ve seen far too much CSS that goes straight to the !important
escape hatch when a higher-specificity selector would have done the trick. If you’re primarily using class selectors, you can easily do this by nesting selectors or adding another class when you need an override.
However, with better knowledge of the CSS cascade comes higher responsibility. The more specific parts of the cascade (such as !important
, inline styles, id selector ) tend to result in stylesheets that are harder to update or override in the future. They do come in handy if you working with component libraries that use inline styles or CSS libraries that you don’t control.
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDesign React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
2 Replies to "How CSS works: Understanding the cascade"
Great article! For so many years I have also, always assumed that the term ‘cascade’ was referring to, what is in fact – inheritance! And I’d bet 90% of devs also make the same mistake.
Perhaps the choice of that particular word to describe the process of conflict resolution could have been better.