CSS Cascade: How the Browser Determines the Final styles

CSS Cascade: How the Browser Determines the Final styles

In this post we’ll go deeper into CSS cascade, one of the CSS core concept that I believe everyone should understand. this concept can be tricky even for experienced developers. We’ll help you understand why your styles don’t work as expected or why certain styles are applied even though you didn’t add them.

I believe the best way to achieve this understanding is by combining theory and practice and that’s what we’ll do in this article.

By the end of this article, you'll have the knowledge and the confidence to resolve CSS conflicts, ensuring your styles always behave as expected.

What is CSS cascade?

The CSS cascade is an algorithm used by the User Agent (browser) to resolve conflicts when the same declaration is added multiple times to the selected element(s). The browser attempts to select the correct declaration to apply. The CSS cascade uses several layers to filter and determine which property will be applied. Sometimes the browser goes through all of these layers to resolve the conflict, but other times it can be resolved in the first layer(s). These layers are: Importance and origin, context, inline style, cascade layer , specificity, scope proximity , and order of appearance.

Let's dive into each one of these layers and do some practice to understand how the browser uses the CSS cascade to determine which properties will apply.

CSS cascade: Origin and Importance

The origin of a declaration is based on where it comes from. In CSS, there are three main origins:

Author stylesheets: styles that we add as developers.

User stylesheets: styles set by the operating system or browser extensions. These allow users to customize their web experience, such as increasing font size or using a dark mode extension ...etc.

User Agent stylesheets: Default styles provided by the browser. These are the styles that browsers apply to HTML elements. For example, the default margin around paragraphs or the styling of form elements.

This layer is responsible for resolving conflicts between origins, To accomplish that, it has a set of rules that simplify this process. you can see this illustrated in the image below, where the order is from top (high priority) to bottom (low priority).

CSS cascade origin and importance hierarchy diagram. From highest to lowest priority: Transition declarations, Important User Agent, Important user, Important author, Animation, Normal author, Normal user, Normal User Agent. The diagram uses color-coded bars to represent each level, with an arrow indicating the order from top (highest priority) to bottom (lowest priority).

The skyblue bars in the illustration above; describe the priority of each origin in normal declaration, and show that the author has the highest priority. The css Group doesn't want our styles(Author) to be prioritize in all cases, we need some sort of balance. This is where Important came in handy. When we add !important to a declaration, it creates another layer of priority with flipped order, Look at the purple bars above, where the author has the lowest priority; this creates the balance that we talk about, and this is the main reason why !important exists not to be used as a hack to force some declaration to be applied in our code.

Let's make our hand dirty by doing an example that demonstrates a set of conflicts and how cascade origin and importance layer are going to resolve them.

<div style="background:pink !important" id="hero">Hello world</div>
/* 🚨LET's PRENTEND THAT THIS IS A USER AGENT STYLE  🚨 */
div {
  display: block;
  width: 100%;
  color: #eee !important;
}
/* 🚨LET's PRENTEND THAT THIS IS A USER STYLE THE USER USE AN CHROME EXTENSION TO FORCE DARK MODE  🚨 */
div {
  color: white !important;
  width: 100vw;
}
/* 🚨 AUTHOR STYLE START 🚨 */
#hero {
  width: 720px;
  color: #efe !important;
  background-color: brown !important;
  background: green;
}

In this example, we aim to highlight all potential scenarios of a conflict, including those with normal declarations, with important declarations, and those with both. But before going through the conflict, let's begin by identifying styles that don't have any conflicts; they will be apply directly without the need for cascade resolution.

display: block;

Let's begin with normal declaration conflicts where no !important rule is applied. The declaration from the author will be applied.

width: 720px; // AUTHOR (Wins) ✅
width: 100vw; // USER (Overridden) ❌
width: 100%; // USER AGENT (Overridden) ❌

After we went through !important in all declarations, the usual cascade priority is flipped. And the User Agent stylesheets become higher.

color: #eee !important; // USER AGENT important (Wins) ✅
color: white !important; // USER important (Overridden) ❌
color: #efe !important; // AUTHOR important (Overridden) ❌

Normal and important declarations are treated separately, even if they come from the same origin. In our example, author styles with !important are in a different stage than normal author styles.

/* Layer for User Agent Importance (No declaration) */
/* Layer for User Importance (No declaration) */
/* Layer for AUTHOR Importance (Two Decalrations) */
background-color: brown !important; // 🚨 AUTHOR important (In Progress) ✅🚧
background-color: pink !important; // 🚨 AUTHOR  important inlineStyle=true  (In Progress) ✅🚧
/* Layer for AUTHOR Normal (Two Decalrations)*/
background-color: green; // AUTHOR (Overridden) ❌
/* Layer for User Normal (One Decalration)*/
/* Layer for User Agent Normal (One Decalration)*/

While we have two potential winners, only one of them will apply. The decision is passed to the next layer(s) of the CSS cascade. Remember, the CSS cascade involves multiple layers, this one only resolves conflicts based on origin and importance.

CSS cascade: Context

In the last section, we spoke about the origin of stylesheet, it's time to talk about the context. In CSS, styles can come from two different contexts shadow tree or document tree. Let's try to understand this context and how it works with CSS Cascade.

  • Document tree: is the main DOM tree, it can host both regular dom elements or shadow dom elements,
  • Shadow tree: it’s a powerful scoping feature that prevents styles from leaking out or in the tree.
  • Inner style: These are style defined inside the shadow tree
  • Outer style: These are style that comes from the document tree
  • In Normal Declaration: outer style (document tree styles) has higher priority than inner style (shadow tree styles).
  • In Important Declaration: The order flipped and the inner styles has more priority, remember the rule we spoke about before.
  • :host Selector: this css selector targets the shadow host, look at the example below the :host is responsible for style the c-button from inside.
  • Styling the shadow host and slot: Even though the shadow DOM is scoped, we can style the shadow host or any element that live directly in the document tree like slot from the outside style (document root), because it part of the main document tree. However, we can't style the element inside the shadow tree like the button in the example below, because shadow tree protects them from outer styles.
<template id="button">
  <style>
    :host {
      color: black !important;
      background: red;
      padding: 0.5rem 1.5rem;
      border: 1px solid transparent;
    }
  </style>
  <button><slot></slot></button>
</template>
customElements.define(
  'c-button',
  class extends HTMLElement {
    connectedCallback() {
      this.attachShadow({ mode: 'open' }).append(
        button.content.cloneNode(true),
      );
    }
  },
);

The code above is responsible for making a custom HTML-element c-button. For more information about the custom element and how you can make it, check those articles from javascript info, the code below, try to call the custom element and restyle it to mimic a conflict and understand how the cascade resolves it.

<c-button class="custom-button"> Hello </c-button>
c-button {
  padding: 1rem 3rem !important;
}

.custom-button {
  background: orange;
  color: white !important;
}

Now that we have a basic understanding of how the context layer works, let's create a code that resolves this issue, then follow these steps below:

  1. Put each declaration in its own layer.
  2. Add this emoji ⚔️ next to declarations that have conflict and 🕊️ to declarations that don't.
  3. Visualize from top to bottom and look for the first declaration that has conflict and add ✅ to highlight it as the applied (winner) declaration and ❌ to other declaration(s).
/* Important Inner Style  */
color: white !important; //  ✅ ⚔️
padding: 1rem 3rem !important; //  ✅ ⚔️

/* Important Outer Style Layer Context */
color: black !important; //  ❌ ⚔️

/* Normal Outer Style Layer Context */
background: red; //  ✅ ⚔️
padding: 0.5rem 1.5rem; //  ❌ ⚔️
border: 1px solid transparent; //  ✅ 🕊️

/* Normal Inner Style Layer Context */
background: orange; //  ❌ ⚔️

CSS cascade: inline style

Inline styles are styles that apply directly to HTML elements. They're the third layer of the cascade, and they're not part of the CSS cascade specificity layer as some people suggest. Let's do some examples to understand how it works.

Let's get back to the example from last time about the background-color. After resolving the conflict in origin and importance, we got two winners. Let's check if we can resolve this conflict in this layer or pass it to the next one.

After visualizing the conflict, I see that we have two declarations, both with !important, and one comes from an inline style. That means the inline style wins.

background-color: pink !important; // AUTHOR  important inline=true (Wins)✅
background-color: brown !important; // AUTHOR important (Overridden)❌

CSS Cascade: Cascade Layer

CSS Cascade Layers is the fourth layer in CSS Cascade, allowing you to take control of your code by making some of your stylesheets have higher priority than others by putting them within a layer. This is a list that shows the basic features of the cascade layers.

  • The priority of each layer determine by its order. The first declared one has the lowest priority while the last ones has the highest.
  • Styles without a layer have the most priority, but in the future we can explicitly control the order of unlayered styles. There's a proposal open by mirisuzanne for more detail
  • Cascade layer has a feature that merges the layers with the same name in the first declared one.
  • By merging layers with the same name, we can use this feature to reorder the under layers, by doing this we make our code clear and simple to visualize the priority of each layer without scrolling the entire stylesheet, look at the example below, where we use @layer reset components utils; to reorder layers.
  • Remember the rule we said before about !important, when it is added to a declaration it's introduces new stages above the normal ones in a reversed order, let's add an example for this layer @layer reset utils;.
    • Important reset;
    • Important utils;
    • Important Unlayered Style;
    • Normal Unlayered Style;
    • Normal utils;
    • Normal reset;

Now that we have a basic understanding of the cascade layer, let's try to add an example that show how CSS Cascade Resolve conflict related to Cascade layer and practice to solidify our understanding.

@layer reset components utils;
@layer utils {
  button: {
    color: orange;
    border-bottom: 2px solid var(--red-8);
  }
}
@layer components {
  button: {
    color: red;
    border-bottom: 2px solid var(--red-8) !important;
  }
}
@layer reset {
  button {
    color: white;
    padding: 1rem !important;
    appearance: none;
  }
}
button {
  color: green;
  padding: 0;
  border-bottom: 2px dashed var(--red-8) !important;
}

To resolve this conflict in a easy way let's follow these steps below:

  1. Put each declaration in its own layer.
  2. Add this emoji ⚔️ next to declarations that have conflict and 🕊️ to declarations that don't.
  3. Visualize from top to bottom and look for the first declaration that has conflict and add ✅ to highlight it as the applied (winner) declaration and ❌ to other declaration(s).
/* Layer for reset Importance  */
padding: 1rem !important; //  ✅ ⚔️
/* Layer for components Importance  */
border-bottom: 2px solid var(--red-8) !important; // ✅ ⚔️
/* Layer for utils Importance */
/* No Layer Style  Important */
border-bottom: 2px dashed var(--red-8) !important; // ❌  ⚔️
/* No Layer Style  Normal */
color: green; // ✅ ⚔️
padding: 0; // ❌ ⚔️
/* Layer for utils Normal */
color: orange; // ✅ ⚔️
border-bottom: 2px solid var(--red-8); // ❌ ⚔️
/* Layer for components Normal  */
color: red; // ❌ ⚔️
/* Layer for reset Normal  */
appearance: none; // ✅ 🕊️
color: white; // ❌ ⚔️

In the example below, the button type selector is applied even when the ID and class have higher specificity. This is because cascade layers are evaluated before specificity in the cascade algorithm, and the button type selector isn't in any layer remember that unlayered styles has the highest priority.

<button id="btn" class="u_btn-orange">hello</button>
button {
  background: green;
}
@layer components {
  #btn {
    background: red;
  }
}
@layer utils {
  .u_btn-orange {
    background: orange;
  }
}
/* Important components Layer */
/* Important utils Layer */
/* Important Unlayered Layer */
/* Normal Unlayered Layer */
background: green; // ✅ ⚔️
/* Normal utils Layer */
background: orange; // ❌  ⚔️
/* Normal components Layer */
background: red; // ❌  ⚔️

CSS Cascade: Specificity

Specificity is an algorithm that calculates the weight of a CSS selector used by the user agent (browser) to determine the winner when conflict exists and isn't solved by the previous layers.

Every CSS selector has a score, and the specificity uses this score to resolve conflict, but before diving into specificity, let's talk first about the CSS selector and the score of each one of them.

No specificity selector

  • Universal Selector (*): This selector select all elements.
  • :is(), :not(), and :has(): This pseudo class selectors has no specificity, but the selector whitin them has specificity.
  • :where(): This pseudo class selector and the selector whitin it has no specificity.
* {
}
section:has(img) {
}
:where(.btn) {
}
:is(button, a, .btn):hover {
}

specificity score 0.0.1

  • Type Selector: Selects all elements that match a specific tag name. For example p to select all <p> elements.
  • pseudo-element: Used to style a specific part of an element or insert content that doesn't exist, prefixed with :: for example ::before to insert element as the first child of the selected element.
/* Type Selector */
p {
}
ol,
ul {
}
/* Pseaudo element Selector */
::before {
}
::after {
}

specificity score 0.1.0

  • Class: matches all elements that has a class attribute with same value as the selector prefixed with ..
  • Pseudo-class: A keyword added to the selected element for a specific state, prefixed with a colon : like :hover , :focus and :disabled.
  • Attribute Selector: select all element that has an attribute with same value as the selector within a [Attribute Here] for example [disabled] or [type="button"].
/* Class Selector */
.btn {
}
/* Pseaudo-Class Selector */
:focus {
}

:disabled {
}
/* Attribute Selector */
[aria-current='page'] {
}
[data-orientation='vertical'] {
}
[href$='.com' i] {
}

specificity score 1.0.0

  • ID Selector: Select the element that has id attribute same to the selector prefixed with #.
/* ID Selector */
#btn {
}

Comma Separator: Doesn't combine weight of all selectors but each selector(s) between comma has it own weight.

/* 0.2.0 */
.btn:hover,
/* 0.1.1 */
button:hover,
/* 0.2.1 */
input[type='button']:hover {
}

Let's make a note before diving into the examples. Our scoring system consists of three independent numbers that adhere to a strict rule:

  • each number has a selector(s) that increases its value, (take a look at the section above about scoring of each selector).
  • 1.0.0 is not equal to 100 and 0.1.5 isn't 15
  • 0.36.0 will never be 3.6.0.
  • 1.0.0 is greater than 0.36.0
  • To compare two scores, we start by the leftmost number, the score with the higher leftmost win. If the leftmost numbers are equal, we compare the middle, and so on.

Okay, now that we have an idea of how specificity scoring systems work and how we choose the winner, let's create some examples to solidify our understanding and explore how we can tackle some common issues we encounter when dealing with specificity.

<button class="btn" id="gmail">Hello world</button>
/* Score is 1.0.0 */
#gmail {
  background: red;
}
/* Score is 0.1.0 */
.btn {
  background: skyblue;
}
/* Score is 0.2.0 */
.btn:focus,
.btn:hover {
  background: brown;
}

Our Intent in this example is to create a button component that has this features:

  • the button will be red because it has ID selector gmail.
  • brown when get focused or hovered.

However, the button remain red in all situations. this is not what we want. Let's try to fix this issue step by step.

  1. the problem is the ID selector #gmail has higher specificity 1.0.0 which means we can’t override it no matter how many classes or other selectors we add. This is one of the reason why people suggest that using ID as a selector is a bad habit, to fix this issue. Let’s replace ID with .btn-gmail class selector.
  2. Now we have another issue, the button unexpectedly becomes blue, because both classes have the same specificity score, means last declaration take precedence remember that cascade contain multiple layers and if the conflict not resolved by this layer we send it to the next one. to fix the issue let add .btn-gmail under the .btn class

Let's work through another example to gain more practice and show other issues that can occur when resolving conflict related to specificity, and discuss some strategies to help us avoid this issues in the future.

<input type="submit" class="btn" />
/* Reset Style Section Specificity score: 0.1.1 */
input[type='submit'],
input[type='button'] {
  background: none;
  cursor: pointer;
}
/* Component Style Section Specificity score: 0.1.0 */
.btn {
  background: blue;
}

The problem with the example above is that the specificity of the input[type='submit'] selector in the reset is higher than the class selector .btn that make the background of the reset apply. To resolve this conflict, we have three possible solutions:

Use cascade layers (@layer): wrap the reset styles in a reset layer, this is the recommended solution for this issue. However, we're looking for alternative solution because similar issues can occur whithin a layer, and creating a new layer for each issues isn't practical.

Increase the specificity of other selectors: it is not recommended solution to increasing the specificity of other selectors just to beat the reset selector. This leads to unscalable, messy, and spaghetti code. Every time you want to add a new class, you need to increase its specificity, which is not maintainable. To keep your CSS clean, you need to aim for a specificity of 0.1.0 for most selectors and only increase it for state changes like :hover , data-open='true', or similar.

Decrease the specificity of the input[type='submit'] selector: This is the elegant and scalable solution. By reducing the specificity of the higher scoring selector, there are two main approaches.

Remove one of the selector

You could reduce the specificity by removing one or more of the selector(s), if possible and doesn't affect the selected element. In this example if we remove [type='submit'], [type='button'] it will matches all input elements. but our reset only aim for input buttons either button or submit, so we can't remove the selector(s).

Use :where() or :is()

Let's explain what :where and :is doing but only the related part of the cascade. For more information, check this article. When we combine selector, the specificity combined gets higher. This is where :is and :where come to the rescue; both of them reduce the specificity of the selectors within them, but with only one small difference :is get the score of the higher selector within them, and :where removes the specificity. To make this clear, let's do some examples.

/* Know the reset Selector has No specificity 0.0.0 */
:where(input[type='submit']) {
  background: none;
}
/* The specificity Become 0.1.0 */
:is(input[type='submit']) {
  background: none;
}
/* The specificity Become 1.0.0 */
:is(#hello #world #my #name #is .hamza .miloud .amar) {
  background: none;
}

CSS Cascade: Scope Proximity

Before scope proximity was introduced, the CSS cascade resolved conflict that still persists after specificity by order of appearance layer. But this is not the case for all situations; sometimes we want the element to be styled depending on the near ancestor. Scope proximity is a new layer added before order of appearance that tries to resolve conflicts by considering how close an element is to its scope root (ancestor). The closer it is, the higher its priority. Let's try to add an example to show you the issue and how we can solve it.

<section lang="en">
  <button>hello</button>
  <section lang="ar">
    <button>استطلاع</button>
  </section>
</section>
button {
  background: var(--p);
}

[lang='ar'] * {
  --p: brown;
}

[lang='en'] * {
  --p: orange;
}

In the example above, all elements descendant of the lang="ar" are also descendant of lang="en". CSS is not designed for scope by default, which means to resolve this conflict, CSS relies on order of appearance; because of that, the last declared rule takes precedence and all the elements will be orange. Well, that's not what we did intentionally; we want all elements inside the [lang='ar'] to be brown because this is the closest ancestor. To achieve this, we have two options in CSS:

Add a Class: We could add a CSS class theme-ar to manually set the background color. However, this is not an optimal solution; we need to add the class to every element inside the Arabic language tree, which increases maintenance, and if we add other languages, it gets worse.

@scope at-Rule: The second solution is the at-rule @scope that adds scope proximity layer to the css cascade. In this way we could solve the conflict we wanted, Let's do that on the example below. This feature is still not availabile across all major browsers.

button {
  background: var(--p);
}

@scope ([lang='ar']) {
  button {
    --p: brown;
  }
}

@scope ([lang='en']) {
  button {
    --p: orange;
  }
}

CSS Cascade: Order Of Appearance

Order of appearance is the last layer of the cascade, and if the conflicts still persist, the last declaration takes precedence. This layer seems simple, but believe me, it's not. In my opinion this is the most difficult layer in the cascade all the previous ones follow a simple rule to resolve the conflict, but this layer is about the order of the the declarations and when the stylesheet get bigger and spread across multiple files, it get realy complex and time consuming to resolve it. because of that we need to structure our stylesheet in certain ways and follow some naming conventions to make it simpler to know which declarations are going to override which only by looking at the name of the class without looking at the entire stylesheet. But don't worry, I want to write a deep dive article on how we can write a scalable CSS code with modern CSS features in the future. Stay tuned. Let's explore some examples and address common issues.

<button class="btn btn_facebook">hello</button>
.btn {
  padding: 1rem; //  (Overridden) ❌
}

.btn_facebook {
  padding: 1.5rem; //  (Overridden) ❌
  padding: 2rem; //  (Win) ✅
}

The key point here is that when a conflict exists, the rule that appears last takes precedence. This is why the last padding: 2rem; is applied.

Another thing to pay attention to is that declaring shorthand versions of CSS properties will often clear or reset previously set values. like with background property, as we seen in the example below.

.btn {
  background-image: url('/button.png');
  background: orange;
}

This behavior is not peculiar to the background property; other shorthand properties act similarly. Once used, they reset all their sub-properties to their default values, except if explicitly set in the shorthand declaration, and this is an example of what it means when we write background:orange.

background-image: none;
background-position: 0% 0%;
background-size: auto auto;
background-repeat: repeat;
background-origin: padding-box;
background-clip: border-box;
background-attachment: scroll;
background-color: orange;

When using shorthand properties remember what other styles you may be overriding or you can write longhand after shorthand like in this example below.

.btn {
  background: orange;
  background-image: url('/button.png');
}

I hope you enjoyed this deep dive article about CSS cascade and helped you improve your knowledge and skills to tackle CSS cascade conflict effectively. Please consider supporting my ongoing efforts to create high-quality educational content by buying me a coffee, look at the bottom under and thanks.

Let's Connect

Your comments and opinions are most welcome! Feel free to let me know whether you have any views on this article, find an inaccuracy, or a better way of doing something let me know! You can reach me on Twitter @HamzaMiloudAma1, and let's continue from there! .

Thank you for reading!

Connect With Me On Social Media

I appreciate your visit to my website. If you find my work interesting and would like to know more about me, please consider following me on social media. Thank you!

Join the Newsletter

Please subscribe to our web site to see all new stuff

© 2024 Hamza Miloud Amar. All Rights Reserved