How CSS works: Understanding the cascade
How CSS works: Understanding the cascade êŽë š
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.
Defining the cascade
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.
CSS Cascade Level 4 Spec
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.
- Origin & Importance
- Selector Specificity
- Order of Appearance
- Initial & Inherited Properties (default values)
Donât worry, weâll get into each of these in-depth.
Origin & importance
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.
- User-Agent: These are the default styles provided for the element by the browser. This is why inputs can look slightly different on different browsers, and itâs also one of the reasons that people like to use CSS resets, to make sure that user-agent styles are overridden.
- User: These are defined and controlled by the user of the browser. Not everyone will have one, but when people do add one, itâs usually for overriding styles & adding accessibility to websites.
- Author: This is CSS declared by the HTML document. When weâre writing stuff as front-end developers this is really the only origin that we have in our control.
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.
- User-Agent &Â
!important
- User &Â
!important
- Author &Â
!important
- CSS Animations,
@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) - Author, normal weight
- User, normal weight
- User agent, normal weight
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.
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.
- Inline styles (anything inside a
style
tag) - ID selectors
- Classes / pseudo-selectors
- Type selectors (for example,
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.
Source order
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.
Initial & inherited properties
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;
}
How does understanding the cascade help me write better CSS?
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.