Battling CSS style collisions with the superpowers of the cascade

A deep dive into Cascade Layers

Jan 12th, 2023

css

Understanding the cascade is an important part of learning CSS, and it's the unsung hero of conflict resolution on the web. However, it can also drive CSS authors to frustration, as it may be the reason that some CSS properties may not work as expected. To put it in context, the cascade is the reason that a text colour for a button can be rendered even though multiple definitions may exist.

Hello there

We have a paragraph element above that we are selecting in three separate ways using CSS:

  1. Just the element selector
  2. An element selector that has the !important rule
  3. A class selector

How does the browser know which style to apply? Is it the first, second or third option? When multiple CSS selectors for the same HTML element exists, the cascade is what governs which of these selectors takes precedence, as the algorithm is designed specifically to resolve this conflict. It looks at all the selectors for a given element and, through a defined algorithm, it selects which rule will be applied.

.App {
  font-family: sans-serif;
  text-align: center;
  padding:1em;
}

.setupStyle {
  font-size: 1.25rem;
}

.setupStyle:last-child {
  color: black !important;
}

p {
  color: rgb(33, 34, 32);
}

p {
  color: darkmagenta !important;
}

.subTitle {
  color: darkgoldenrod;
}

The definition of the CSS cascade that I like comes from Bramus another web developer. It goes, "The CSS cascade is an algorithm that determines the winner from a group of competing declarations". Competing declarations is the important part here because as we alluded to above, there are multiple ways in CSS to select the same element.

The CSS Cascade Algorithm

The cascade algorithm is split into 4 stages:

  1. Order of appearance -- Where does the rule appear in your style sheet? The order of the rules
  2. Specificity: An algorithm that determines which CSS selector has the strongest match
  3. Origin: Where does the CSS selector come from? Is it the browser that is setting the rule, an extension, or authored style
  4. Importance: Does the rule have the !important rule attached to it?

These four rules are used to determine the winner from a group of competing declarations thereby preventing conflicts about what styles are applied to an element. This article walks you through the intricate details of the CSS cascade algorithm. Amelia Wattenberger also has an incredible article going over the CSS cascade.

Even with a working knowledge of the cascade, we've all experienced CSS style collisions in our codebase when writing new styles or adding new 3rd-party styling libraries. This can make it incredibly frustrating to work with CSS sometimes. However, there have been some new additions to the CSS spec that makes working with the cascade easier.

Cascade layers allow CSS authors (developers) to add layering to CSS declarations. In short, they rework how we approach organizing and structuring declarations within our CSS files, thereby limiting conflicts and giving the developer a bit more control over the cascade.

Before we dive into cascade layers, let's go through a quick primer on the CSS cascade.

Primer

We've established that the cascade algorithm's job is to determine the correct values for the CSS properties when there are multiple selectors/declarations for the same element. The algorithm has many criteria -- one being the position/order of appearance of the declaration. CSS declarations that appear further down in the CSS file have higher priority than declarations that appear near the beginning of the CSS file -- we are assuming the declaration are for the same element.

CSS declarations can be made from multiple origins, therefore to properly determine the correct value for a CSS property when there is a conflict, the algorithm must be able to decern which origins have higher priority. Let's bring your attention back to the definition of the cascade algorithm, more specifically let's focus on the origin rule. This rule alludes to the fact that style sheets can originate from different origins (These are known as cascade origins) and they include: (listed in order of precedence from low to high)

  1. User-agent styles
  2. User styles
  3. Author styles

User-agent style

User-agent styles, better known as browser styles, establish the default styles for an HTML document and they are written by browser vendors. Although some browsers allow users to modify user-agent styles, it is very rare and not something that can be controlled. For this reason, you will find that brand-new HTML files that do not have any authored CSS linked, still have styles that are being applied in the browser. Elements such as headers, and paragraphs may have padding or margin added to them even though you did not specify it. Moreover, these styles are browser specific, therefore, they may be implemented differently across browsers. A range selector in Google Chrome may look different in Safari or Firefox, and it is a result of different styles being applied by the browser.

Unless the user-agent stylesheet includes an !important rule next to a property, any CSS declaration in the Author style or User styles can override declarations in the user-agent styles.

devTools showing user-agent style

Looking at the browser dev tools, you will be able to see the styles that are defined by the user-agent style sheet. Most often, they will be crossed out if they are overwritten by author or user styles.

User styles

The user stylesheet consists of styles written by a user for a specified website and they can be used to override user-agent styles. User styles can be modified/configured directly or added via browser extensions.

Author styles

Author styles are the most common type of style sheets that you will come across as these are the style sheets that are written by web developers. Author styles have a higher priority than user-agent styles and thus can be used to reset any styles set by the user-agent. In addition, author styles can also define the styles for the design of a given web page. As you may recall, user-agent style sheets are sometimes different across browsers, thus to ensure consistency you will often see CSS resets/normalization being added to a project. CSS resets, undo all or most default styles and creates a blank slate. A reset may look something like this:

css

*,
*::before,
*::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}

This reset is setting the padding and the margin on all elements to 0 including the before and after pseudoelements. It is also setting the box-sizing to border-box, therefore, the border is taken into account when calculating the width of a container. There's a lot more that goes into a CSS reset, and there're many different ones -- here's one reset that I find is used quite frequently. Although CSS resets are a good thing to have in your project, I do find that they may not be needed as much anymore. Browsers no longer have massive discrepancies when it comes to layout or spacing since they implement the CSS spec very similarly so things behave as you'd expect.

Author style sheets can be written in different ways:

  1. Inline -- defined using the style attribute on an HTML element
  2. Internally -- using the style tags in an HTML document
  3. Externally -- CSS document is linked/imported into an HTML document

Note on writing styles

It's important to note that the method chosen to write the author styles also has implications on the specificity of the selectors for an element. Inline styles take precedence over internal and external styles while internal styles take precedence over external styles.

The problem

Cascade origins help with the organization and balancing of styling concerns across different agents (style sheets that are setting styles). As we've seen above, it allows the cascade algorithm to discern which declarations take priority. Although it's awesome that we have this separation of layers between different origins, it may be useful to have this same layering of concerns when working within the same origin. Let's take the author styles for example -- as we alluded to above, we often first apply CSS reset/browser normalization and then add styles that fit our design system. This apparent layering of concerns means that we have to be aware of the selectors we choose so as not to run into a specificity battle later on. This also means that we are forced to carefully manage selector-specificity or use the !important flag for overrides to get things to work as expected.

Causion

Be very careful using the !important flag because it's often & easily misused, and it can lead to more unexpected side effects.

Show more

We can use conventions such as BEM, ITCSS, or OOCSS to try to alleviate the occurrence of specificity battles, however, the issue of carefully managing selector-specificity still remains.

Is there another way???

Cascade layers

Cascade layers allow for the balancing and organization of styling concerns within the same origin and its part of the CSS Level 5 specification. As you may recall, we mentioned that cascade layers allow CSS authors (developers) to add layering to CSS declarations. It's important to note that we are not talking about visual layering which is often seen with the z-index. Rather, cascade layers refer to the way we structure our CSS code. We are reworking how we approach organizing and structuring declarations within our CSS file thereby limiting conflicts and thus taking back some control over the cascade.

To begin with cascade layers, we first use the @layer property to tell the browser that we want to define a new layer. We can now specify CSS declarations inside the layer. The added benefit is that declarations that are added to a layer are now scoped to that layer. I, however, say "scoped to that layer" with a grain of salt as this is different from scoping in CSS -- more on this later.

Cascade layers can be written in a couple of different ways but to start us off, we'll look at how we can define a layer and then write rules inside our layer at the same time. The following code block describes how we can do this:

css

@layer reset {
.main{
something goes here
}
}

You can see that it's like using another @ rule in CSS. The @layer property is followed by an optional name parameter -- in this case reset. Layers can either be named or they can remain anonymous. Named layers have a name/identifier that follows the @layer property while anonymus layers do not have this parameter. For this reason, named layers can be referenced multiple times in multiple locations and it allows us to merge styles into layers that have the same layer name. Since anonymous layers do not have the name parameter, they cannot be referenced later on thus merging of styles cannot occur with these types of layers. Anonymous layers look like the following:

css

@layer {
.main{
something goes here
}
}

You might now be wondering if we can reference named layers multiple times, then what sets the order and precedence of a cascade layer?

Similar to the order & precedence of CSS style rules, the order & precedence of a cascade layer is set in the order that they appear. To be specific, layers that appear last will always have higher priority than layers that appear near the beginning -- an increasing priority. In our demo below, we've defined a couple of cascade layers in our CSS file. Let's see what happens to the font size of the text when we move the base layer to the bottom of the CSS file

.App {
  font-family: sans-serif;
  text-align: center;
  padding: 1em;
}

.setupStyle {
  font-size: 1.2rem;
  color: black;
}

@layer base {
  .main {
    font-size: 1.5rem;
  }
}

@layer main {
  .main {
    font-size: 3rem;
  }
}

As we can see, when we move the base layer below the main layer, the size of the text becomes smaller. This occurs because the base layer now has higher priority/precedence over the main layer. So if this is how we set the order & precedence of cascade layers, wouldn't we have to keep track of where we define our layers if they are defined across multiple files? Moreover, wouldn't we have to make sure that the file that contains the base layer is imported before the file that contains the main layer?

In short, yes. However, there is another approach that allows you to set the order & precedence of cascade layers before you define the styles that are associated with the respective layer. The @layer rule can be used with only an identifier/layer name to define a layer without attaching any style rules. This is useful for establishing a layer order in advance. It looks like the following:

css

@layer base, main;

Recall that earlier we were speaking about named layers being able to be referenced multiple times in multiple locations and that a merging of styles would occur for layers with the same name. This is possible because with layer-names that match an existing layer defined in the same layer-scope and origin, will assign the style rules to the existing layer. Effectively, we are first defining the layers without setting any style rules to set the order & precedence of the layers. We can then reference the layer name and set the style rules for that layer. We do not have to worry about the order in which we set these layers with style rules as the layer has already been defined. The style rules defined will then be assigned to the existing layers. This only works with named layers and it is important that the names remain the same as any new layer name defined will automatically have the highest priority.

This demo is similar to the one above as we've defined a couple of cascade layers in our CSS file. However, we have also defined the order & precedence of the layers -- this is seen on the first line of the CSS file. Let's see what happens to the font size of the text when we move the base layer to the bottom of the CSS file.

@layer base, main;
.App {
  font-family: sans-serif;
  text-align: center;
  padding: 1em;
}

.setupStyle {
  font-size: 1.125rem;
  color: black;
}

@layer base {
  .main {
    font-size: 1.5rem;
  }
}

@layer main {
  .main {
    font-size: 3rem;
  }
}

As we can see, when we move the base layer below the main layer, the size of the text does not change. This occurs because the priority/precedence of the base layer does not change even though we've moved the base that defines the styles. The order & precedence of the layers is set at the very top of the file. This would still work if the base and main layers that have styles defined were in separate files. We just have to make sure that the file that sets the order & precedence for all the layers is imported first.

Layering CSS imports

With cascade layers, you have the ability to import CSS libraries and put them inside a layer! In my opinion, this is just awesome! You may have found that it can be frustrating working with CSS libraries when you are trying to override certain rules set by the library. The documentation of some of these libraries doesn't explicitly tell you this but sometimes they use highly specific CSS selectors, therefore, it may be nearly impossible to override some of these rules. This issue is no longer the case with cascade layers as we can import libraries such as Bootstrap, Material UI, or other libraries, and place them inside of a layer. The layers we define later on will have a higher precedence than our libraries layer, therefore, we now have the ability to easily override selectors in these CSS libraries.

The following code block is showing us how we may import a CSS file and assign its styles to a layer:

css

@import url(bootstrap.css) layer (library);
@layer base, main, table;

As stated above, the base and main layers will take precedence over the library layer. An alternative approach would be to define all the layers first and then do the imports. Using this alternative approach means that the order in which you @import your styles won’t matter to the layer order, since the order of the layers is already established.

Nesting Layers

Sometimes we may want to nest layers within layers when using cascade layers as layer names may start to become verbose and repetitive. For example, let's take the main layer we defined in our last code block -- we can nest a reset layer inside of the main layer. It looks something like the following:

css

@layer main {
@layer reset {
things go in here
}
}

To access the reset layer inside of the main layer, we combine the two-layer names and separate them by a period. It looks like the following: @layer main.reset. Further, layer names are scoped to their surrounding layer, therefore, you will not run into name conflicts if you have a nested layer name that is the same as an un-nested layer name.

Order of precedence for everything

So far we've reviewed the cascade and how the cascade algorithm works, the problems that may arise and the patchy workarounds that exist. We've also gone over the role of cascade layers and how they attempt to solve some of those problems. However, there is one more question that remains to be answered -- When the cascade algorithm analyzes the order & precedence of styles in the browser, where do cascade layers fit in that order? Recall cascade layers are most often defined in the author styles/origin.

Una Kravets has an incredible depiction of the order & precedence of layers and cascade origins over at the chrome developer blog and it's also the following image below:

Order of precedence for overall styles

The order of precedence from lowest to highest is as follows:

You may notice that the above list does not include nested layer styles and thus you may be wondering where those fit in. Nested layer styles have less precedence than their parent layer, therefore, declarations in a parent layer will override declarations in a nested layer.

Recall the example code blocks we've listed above -- So far we have:

css

@layer reset, library, base, main, table;
@layer main {
@layer reset {
/*stuff goes here*/
}
}
@layer {
}
.otherStyles {
}

Let's take a look at the order & precedence of those layers. This is listed from lowest to higher and it's as follows:

It is important to note that unlayered author styles will almost always take precedence over any layered styles that are defined. According to some developers within the CSS working group, this was a very debated topic, however, it was done intentionally. Apparently, it allows developers to have the confidence that their styles will always be applied when they are using style sheets that implement cascade layers -- developers do not need to battle with the order of appearance of styles when importing external style sheets.

Further, I say unlayered author styles will almost always take precedence because of the !important keyword being used with layered styles -- more on this later.

The little big things

With any tool in general, you may benefit from knowing about the nuances of the tool before diving into using it. This is no different with cascade layers!

Order & precedence

In the order & precedence section, you may have noticed that @layer !important styles have higher precedence than non-layered (normal) styles. While layered styles have lower precedence than unlayered styles in general, using the !important rule inside of a layer results in the layer becoming more specific than unlayered styles. Further, if you have multiple layers, the first layer with !important flag would become the layer that has the highest precedence. This is the case as the !important keyword inverts the order & precedence of the layers.

css

@layer base, main, table;

Looking at the code block above, the order & precedence would follow the order in which the layers were listed -- with the base layer having the lowest precedence and the table layer having the highest precedence. However, if the base layer had styles that were using the !important flag, it would mean that those styles would now have higher precedence than their counterparts in the main and table layers.

Specificity & Layers

With cascade layers, a less-specific selector, like an element selector, will override a more-specific selector, like a class selector, if that less-specific selector is inside a more specific layer. For example, if we were using an element selector in our main layer and a class selector in our base layer, then the styles from the element selector in our main layer would be rendered to the browser.

Pop Quiz!!

Question: Using what we just established above about specificity & layers alongside what we know about the importance property with @layers, what styles will be rendered to the browser if we use the !important rule on a property inside a class selector for the base layer?

Scoping

We previously talked about how cascade layers do not solve the scoping issue in CSS. To further expand on this, if you have a CSS file using the @layer property to apply styles to a component, using an element selector will not scope those styles to that component. Rather, the styles will be applied to all instances of that element. Therefore, it remains important that we scope our styles correctly.

Wrapping up

As the name suggests, the cascade is one of the things that define how CSS works and the algorithm behind it is integral to resolving conflicts that exist. Therefore, it's important that CSS authors have an understanding of how the cascade works and how it approaches conflict resolution. As we've seen, without this understanding, the cascade can drive CSS authors to frustration, as some style overrides may not work as expected.

Luckily, the new CSS specification implements a new rule that allows CSS authors (developers) to have a bit more control over the cascade. The introduction of cascade layers allows for the balancing and organization of styling concerns within the same origin. As we've seen, developers have the ability to organize their styling rules into layers so as to limit styling conflicts and ensure that things work as expected.

There are a lot of cool things that can be done with cascade layers but sometimes we may want to roll back to styles that are defined in the user-agent origin. Moreover, we may also want to roll back to styles defined in previous cascade layers. The revert and revert-layer keywords can help us accomplish this and Una Kravets has an incredible video going over how these keywords work.

All right, I'm going to wrap it up there! Hope you found this useful, and I'll catch you in the next one... Peace!

Practice problems

PSSSST! Hey you! Yaa you! Enjoyed the article?? Here's a fun little exercise for you to try out! 👀

Exercise

These are a series of questions or mini-games that are associated with the article you just finished reading! They are meant to help solidify the concepts talked about in this article. Have fun!

on this page

primeruser-agent styleuser stylesauthor stylesthe problemcascade layerslayering css importsnesting layersorder of precedence for everythingthe little big thingsorder & precedencespecificity & layersscopingwrapping uppractice problems

Last updated January 12th, 2023