The Future of CSS: Cascade Layers (CSS @layer)

When writing CSS, we developers have to carefully think about how we write and structure our code. Without any proper “plan of attack” the Cascade can suddenly work against us, and we might end up with pieces of code overwriting each other, selectors getting heavier and heavier, a few !important modifiers here and there, … — Uhoh!

To regain control over the Cascade in those situations there’s a new CSS Language Feature coming to help us: Cascade Layers (CSS @layer).

Let’s take a look at what they are, how we can use them, and what benefits they bring …

~

Table of Contents

~

# The CSS Cascade, a Quick Primer

The CSS Cascade is the algorithm which CSS uses to resolve competing declarations that want to be applied to an element.

/* HTML: <input type="password" id="password" style="color: blue;" /> */
input { color: grey; }
input[type="password"] { color: hotpink !important; }
#password { color: lime; }

Will the input color be grey, lime, hotpink, or blue? Or the User-Agent default black?

To determine which declaration should “win” (and thus be applied), the Cascade looks at a few criteria. Without taking Cascade Layers into account just yet, these criteria are:

  1. Origin and Importance
  2. Context
  3. Style Attribute
  4. Specificity
  5. Order of Appearance (aka Source Code Order)

These criteria are ranked from high to low priority, and are checked one after the other until a winning declaration has been determined. In case it is undecided which property declaration will “win” at a higher criterion, the Cascade will move on to the next criterion.


The CSS Cascade Visualized

For more in-depth info, please refer to this very good post by Amelia Wattenberger on the subject.

💡 And oh, when it comes to Specificity I’m still very fond of the Specificity Wars post that first taught me about it.

~

# Taming the Cascade

When authoring CSS we place our CSS mainly into one and the same origin: the Author Origin. As a result, we end up juggling with Selector Specificity and Order of Appearance as our ways to control the cascade. While doing so, we have to perform a fine balancing act between both of these aspects:

  • Statements that use selectors of a high specificity can cause problems in case you want to override some properties later in the code. This often leads to even more heavy selectors or the use of !important, which in itself can raise even more issues.
  • Statements that use selectors of a low specificity can be overwritten too easily by statements that appear later in the code. This can especially be troublesome when loading third-party CSS after your own code.

To help us tame those aspects of the Cascade, a few clever developers have come up with methodologies such as BEM, ITCSS, OOCSS, etc. over time. These methodologies mainly lean on the following aspects:

  1. Structuring your code in such a way that you create some sort of logical order that works for most scenarios.
  2. Keeping Selector Specificity as low as possible by leaning primarily to classes.


The almighty Inverted Triangle of CSS.

While these approaches can certainly help you strike a balance between Selector Specificity and Order of Appearance, they are not 100% closing:

  • The established order is never really enforced as Order of Appearance still determines things.
  • Selector Specificity still has the upper hand over the order of the layers

~

# Introducing Cascade Layers

To make this balancing act more easy, there’s a new mechanism named Cascade Layers being worked on. It’s a CSS feature led by Miriam Suzannewhom you might also know from CSS Container Queries — and is part of the upcoming CSS Cascading and Inheritance Level 5 (css-cascade-5) Specification.

With Cascade Layers you can split your CSS into several layers via the @layer at-rule. As per spec:

In the same way that Origins provide a balance of power between user and author styles, Cascade Layers provide a structured way to organize and balance concerns within a single Origin.

Because of its unique position in the Cascade, using Layers comes with a few benefits that give us developers more control over the Cascade.


The new CSS Cascade with Layers added to it

Let’s dive in with some code examples, explaining the benefits along the way.

🚨 Before we continue: try and forget any assumption you have about “layers in CSS”. By simply looking at their name, it’s easy to confuse Cascade Layers with layering via z-index or the (deprecated) HTML <layer> element. These things are not the same:

  • Layering via z-index is about visually stacking boxes onto a webpage.
  • The HTML <layer> element is ancient history.
  • Cascade Layers is about structuring your CSS Code and controlling the CSS Cascade.

~

# Creating a Cascade Layer

A Cascade Layer can be declared in several ways:

  1. Using the @layer block at-rule, with styles assigned immediately to it:

    @layer reset {
      * { /* Poor Man's Reset */
        margin: 0;
        padding: 0;
      }
    }
  2. Using the @layer statement at-rule, without any styles assigned:

    @layer reset;
  3. Using @import with the layer keyword or layer() function:

    @import(reset.css) layer(reset);

Each of these standalone examples, creates a Cascade Layer named reset.

💡 A possible 4th way is still being worked on: by means of an attribute on a <link> element. See CSSWG Issue #5853.

~

# Managing Layer Order

Cascade layers are sorted by the order in which they first are declared.

In the example below we create four layers: reset, base, theme, and utilities.

@layer reset { /* Create 1st layer named “reset” */
  * {
    margin: 0;
    padding: 0;
  }
}

@layer base { /* Create 2nd layer named “base” */
  …
}

@layer theme { /* Create 3rd layer named “theme” */
  …
}

@layer utilities { /* Create 4th layer named “utilities” */
  …
}

Following their declaration order, the Layer Order becomes:

  1. reset
  2. base
  3. theme
  4. utilities


Cascade layers are sorted by the order in which they first are declared.

When re-using the name of a Layer, styles will be appended to the already existing Layer. The order of the Layers remains the same, as it’s only the first appearance which determines the order:

@layer reset { /* Create 1st layer named “reset” */
  …
}

@layer base { /* Create 2nd layer named “base” */
  …
}

@layer theme { /* Create 3rd layer named “theme” */
  …
}

@layer utilities { /* Create 4th layer named “utilities” */
  …
}

@layer base { /* Append to existing layer named “base” */
  …
}

The fact that the Layer order remains the same when re-using a Layer name makes the @layer statement at-rule syntax darn handy. Using it, you can establish Layer Order upfront, and append all CSS later to it:

@layer reset;     /* Create 1st layer named “reset” */
@layer base;      /* Create 2nd layer named “base” */
@layer theme;     /* Create 3rd layer named “theme” */
@layer utilities; /* Create 4th layer named “utilities” */

@layer reset { /* Append to layer named “reset” */
  …
}

@layer theme { /* Append to layer named “theme” */
  …
}

@layer base { /* Append to layer named “base” */
  …
}

@layer theme { /* Append to layer named “theme” */
  …
}

Heck, you can write it even shorter, using a comma-separated list of Layer Names:

@layer reset, base, theme, utilities;

🔥 Best Practice: To keep control over Layer Order, it’s recommended to declare all your layers upfront by using this one-line syntax, and —once the order is established— then append styles to them.

~

# Cascade Layers and the Cascade

In the Cascade (the algorithm), Layers get a higher precedence than Specificity and Order of Appearance. So the criteria of the Cascade become this (in order):

  1. Origin and Importance
  2. Context
  3. Style Attribute
  4. Layers
  5. Specificity
  6. Order of Appearance


The new CSS Cascade with Layers added to it

When evaluating the Layers criterion, the Cascade will look at the Layer Order to determine the winning declaration. Declarations whose cascade layer is last, will win from declarations in earlier-declared Layers (cfr. how Order of Appearance works: last one wins).

Cascade layers (like declarations) are ordered by order of appearance. When comparing declarations that belong to different layers, then for normal rules the declaration whose cascade layer is last wins […]


How the Cascade evaluates Layers

Take this snippet from earlier:

@layer reset, base, theme, utilities;

In total we create 4 layers, in this order:

  1. reset
  2. base
  3. theme
  4. utilities

For example: Competing declarations in the theme Layer (3) will win from declarations in the base (2) and reset (1) Layers because those Layers were declared before theme. Competing declarations in the theme Layer (3) however won’t win from those in utilities (4), as that Layer has been declared later.

Once a winning declaration has been determined via Layer Order, the Cascade won’t even check Specificity or Order of Appearance for those declarations anymore. This is because Layers is a separate and higher ranked criterion of the Cascade.

Practical example:

@import(reset.css) layer(reset); /* 1st layer */

@layer base { /* 2nd layer */
  form input {
    font-size: inherit; 
  }
}

@layer theme { /* 3rd layer */
  input {
    font-size: 2rem;
  }
}

Although the input-selector (Specificity 0,0,1) used on line #10 is less specific than the form input-selector (Specificity 0,0,2) from line #4, the declaration on line #10 will win because the theme Layer (3) is ordered after the base layer (2).

🔥 Because later-declared Layers always win from earlier-declared Layers, you —as a developer— don’t need to worry about the Specificity nor Order of Appearance that is used in those other Layers: it’s the Layer Order that dictates who the winner in case of conflict is.

This also means that you can easily move Layers around, knowing that their Layer Order —and not the Specificity nor Order of Appearance— will determine things.

‼️ Do note that this doesn’t mean that Specificity and Order of Appearance are no longer important. These two criteria still are, but only inside one and the same Layer. When comparing declarations between Layers, these two criteria can be ignored.

~

# Intermediate Summary

If you were able to follow along there, this intermediate summary should make sense:

  • With Cascade Layers you can split your CSS into several layers.
  • Upon creating a Layer with @layer, you also determine the Layer Order.
  • Re-using Layer names will append to the already created Layer, without altering Layer Order.
  • When evaluating Layers, the Cascade (the algorithm) will have declarations placed in later-declared Layers win from declarations in early-declared Layers (i.e. “Last Layer Wins”).
  • The Cascade evaluates Layers before Specificity and Order Of Appearance. That way you no longer need to worry about these two criteria for CSS found in separate Layers, as Layer Order will already have determined the winning declaration.

Cool, right?! 🤩

~

💁‍♂️ Like what you see so far? Happen to be conference or meetup organiser? Feel free to contact me to come speak at your event, with a talk covering the contents of this post.

~

# Details you need to know

There’s a few details that one needs to know about the inner workings of Cascade Layers.

# Unlayered Styles come first last in the Layer Order

Update 2021.10.07: The CSS Working Group has decided that Unlayered Styles should come last (instead of first as it was specced before). This post has been updated accordingly.

Styles that are not defined in a Cascade Layer will be collected in an implicit layer. This implicit layer will be positioned first last in the Layer Order.


Unlayered Styles come last in the Layer Order

Because of this position, Unlayered Styles will always override styles declared in Layers.

@import(reset.css) layer(reset); /* 1st layer */

/* Some Unlayered Styles */
h1 { color: hotpink; }

@layer base { /* 2nd layer */
  h1 { font-family: … }
}

@layer theme { /* 3rd layer */
  body h1 { color: rebeccapurple; }
}

@layer utilities { /* 4th layer */
  [hidden] { display: none; }
}

The Layer Order for this snippet looks like this:

  1. reset
  2. base
  3. theme
  4. utilities
  5. (unlayered styles)

The result from the example above will be that the h1 will be colored hotpink, even though the unlayered styles come earlier the Source Order and the used selector in theme Layer has a higher Specificity.

💡 In the future we might gain the ability to control the layer position of these unlayered declarations. This is being tracked in CSSWG Issue #6323

~

# Naming a Layer is optional

Cascade Layers can also be created without giving them a name. These are called “Anonymous Layers”.

  1. Using the @layer block at-rule, with styles assigned immediately to it

    @layer {
      * { /* Poor Man's Reset */
        margin: 0;
        padding: 0;
      }
    }
  2. Using @import:

    @import(reset.css) layer;

A disadvantage of not using a name is that you can’t append to these anonymous layers:

@layer { /* layer 1 */ }
@layer { /* layer 2 */ }
@import url(base-forms.css) layer; /* layer 1 */
@import url(base-links.css) layer; /* layer 2 */

💡 Using the @layer statement at-rule without a name (e.g. @layer;) is possible, but not mentioned as it’s a useless statement to make:

  • It has no content to begin with
  • You can’t append extra content since you can’t refer to it.

~

# Layers can be nested

It’s perfectly fine to nest @layer statements.

@layer base { /* 1st Layer */
  p { max-width: 70ch; }
}

@layer framework { /* 2nd Layer */
  @layer base { /* 1st Child Layer inside 2nd Layer */
    p { margin-block: 0.75em; }
  }

  @layer theme { /* 2nd Child Layer inside 2nd Layer */
    p { color: #222; }
  }
}

In this example there’s two outer layers:

  1. base
  2. framework

The framework layer itself also contains two layers:

  1. base
  2. theme

💡 The re-use of the name base does not conflict here, as that 2nd base is part of the framework layer. Yes, the names are scoped to their surrounding outer-layer (if any)

Representing the Layers as one combined tree, it would look like this:

  1. base
  2. framework
    1. base
    2. theme


Layers can be nested

To refer to a Layer that is contained inside an other Layer, use it’s full name which uses the period to determine the hierarchy, e.g. framework.theme.

The flattened Layer Tree for this code example would then look like this:

  1. base
  2. framework.base
  3. framework.theme

To append styles to a nested Layer, you need to refer to it using this full name:

@layer framework {
  @layer default {
     p { margin-block: 0.75em; }
  }

  @layer theme {
    p { color: #222; }
  }
}

@layer framework.theme {
  /* These styles will be added to the theme layer inside the framework layer */
  blockquote { color: rebeccapurple; }
}

~

# A few more notes / caveats

If you still haven’t had enough, there are a few extra things worth mentioning.

😵‍💫 Already had enough? Feel free to skip this part and immediately jump to Browser Support as it becomes pretty advanced/complicated.

# Cascade Layers and the use of !important

When evaluating the Origin criterion, the Cascade orders the several Origins as follows (ranked from high to low):

  1. Transitions
  2. Important User-Agent
  3. Important User
  4. Important Author
  5. Animations
  6. Normal Author
  7. Normal User
  8. Normal User-Agent

Notice how Origins with !important have the reverse order of their normal (i.e. non-important) counterpart? That’s because of how CSS works:

When a declaration is marked !important, its weight in the cascade increases and inverts the order of precedence.

This inversion-rule is also applied declarations in Cascade Layers: declarations with !important annotation will be put in the “Important Author” Origin, but the Layers will have the inverse order when compared to the “Normal Author” Origin.


Cascade Layers vs. use of !important

Winging back to our four layers from earlier:

@layer reset, base, theme, utilities;

Normal declarations in these layers all go in the “Normal Author” Origin, and will be ordered as such:

  1. Normal reset Layer
  2. Normal base Layer
  3. Normal theme Layer
  4. Normal utilities Layer

Important declarations in these layers however all will go in the “Important User” Origin, and will be ordered in reverse:

  1. Important utilities Layer
  2. Important theme Layer
  3. Important base Layer
  4. Important reset Layer

Because “Normal Unlayered Styles” implicitly go last, this also means that “Important Unlayered Styles” will go first then.

💡 So yes, an !important declaration inside a layer will win from an !important declaration inside Unlayered Styles.

~

# Cascade Layers vs. Media Queries (and other conditionals)

When a @layer is nested inside a Media Query (or any other conditional), and the condition does not evaluate to true, the @layer will be not be taken into account for the Layer Order. Should the Media Query/Conditional evaluate to true later on — because of the screen size changing for example — Layer Order will be recalculated.

For Example:

@media (min-width: 30em) {
  @layer layout {
    .title { font-size: x-large; }
  }
}

@media (prefers-color-scheme: dark) {
  @layer theme {
    .title { color: white; }
  }
}

If the first Media Query matches (based on viewport dimensions), then the layout layer will come first in the Layer Order. If only the color-scheme Preference Query matches, then theme will be the first layer.

Should both match, then the Layer Order will be layout, theme. If none matches no Layers are defined.

~

# Cascade Layers vs. “Name-Defining Rules”

Name-Defining Rules — such as @keyframes, @scroll-timeline, @font-face — follow Layer Order as you’d expect:

@layer framework, override; /* Establish Layer Order */

@layer framework {
  @keyframes slide-left {
    from { margin-left: 0; }
    to { margin-left: -100%; }
  }
}

@layer override {
  @keyframes slide-left {
    from { translate: 0; }
    to { translate: -100% 0; }
  }
}

.sidebar { animation: slide-left 300ms; }

The Layer Order looks like this:

  1. framework
  2. override

As the last layer wins, the slide-left Keyframes from the override Layer (2) — the ones using translate — will be used.

~

# No Interleaving of @import/@namespace and @layer

For parsing reasons (see CSSWG Issue #6522) it’s not allowed to interleave @layer with @import/@namespace rules.

From the moment the CSS parser sees a @layer that follows an earlier @import, all subsequent @import rules after it will be ignored:

@layer default;
@import url(theme.css) layer(theme);
@layer components; /* 👈 This @layer statement here which comes after the @import above … */
@import url(default.css) layer(default); /* ❗️ … will make this @import rule (and any other that follow) be ignored. */
@layer default {
  audio[controls] {
    display: block;
  }
}

To counteract this, group your @import rules together.

@layer default;
@import url(theme.css) layer(theme);
@import url(default.css) layer(default);

@layer components;
@layer default {
  audio[controls] {
    display: block;
  }
}

🔥 Best Practice: Should you rely on @import (which you shouldn’t, as it’s a performance hit) best is to:

  1. Establish a layer order upfront using @layer statement at-rules
  2. Group your @imports after that
  3. Append styles to already established layers using @layer block at-rules
@layer default, theme, components;

@import url(theme.css) layer(theme);
@import url(default.css) layer(default);

@layer default {
  audio[controls] {
    display: block;
  }
}

~

# Browser Support

I’m very happy to see that all browser vendors are working on adding have experimental support for Cascade Layers 🥳. It’s all still experimental support, but given that the spec has matured since it got first proposed in 2019, things are looking very good to see it shipped by the end of this year / early next year.

Chromium (Blink)

Available in Chrome 96+ (current Canary) with the --enable-blink-features=CSSCascadeLayers run-time flag. Starting with version 96.0.4661.0, you can toggle it via the #enable-cascade-layers feature flag in chrome://flags/

As per design doc, aimed target release is M98 (late 2020) or M99 (early 2021)

Firefox (Gecko)

Available in Firefox 94+ (current Canary) by setting layout.css.cascade-layers.enabled to true via about:config.

Safari (WebKit)

Available in Safari Technology Preview 133. To enable it, use Safari’s App Menu in the Menu Bar and choose Develop → Experimental Features → CSS Cascade Layers.

The demo below — by Miriam — will show a green checkmark when @layer support is enabled.

See the Pen OMG, Layers by Miriam Suzanne (@miriamsuzanne) on CodePen.

To stay up-to-date regarding browser support, you can follow these tracking issues:

~

# In Closing

With Cascade Layers coming, we developers will have more tools available to control the Cascade. The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

Personally I’m really looking forward to give Cascade Layers a try. Being able to enforce the ordering used in ITCSS at the language level for example, feels like a great win.

~

To help spread the contents of this post, feel free to retweet the announcement tweet:

~

🔥 Like what you see? Want to stay in the loop? Here's how:

🙏 Thanks to Miriam, Hidde, Sam, Tim, Stefan, Nils, and Adam for their valuable feedback on early versions of this post.

About the author

Bramus is a Freelance Web Developer from Belgium. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Join the Conversation

19 Comments

  1. They should have just called this cascade order ( maybe C Order? or c-order as a way to make it fit the z-order naming convention? ) instead – the layers naming is confusion for no reason.

    1. True! If our average framework-loving developer can’t maintain nice and tidy CSS now, then these layers will destroy him 😀

    2. Completely agree!
      Reminds me of a joke:
      – We have 24 concurrent standards describing same thing, any ideas how deal with that?
      – Yes! Let’s make a new standard which will replace all previous ones and solve their problems!
      …after a while: now we have 25 concurrent standards

      1. I’m excited about this one. I just joined a team with lots of selectors like #sidebar-menu > ul > li > a and it’s too much work to refactor it all.

  2. I am very excited to see this feature! However I would be curious on how to solve one common problem that led to the abomination of JSS:
    can and should we use layers for every component?

    So let’s say you have an application with global styles and components like main panels, main layout elements defined and you have several teams dropping new features onto this.

    People got angry with CSS often because someone fixing one bug in their codebase moved something else in an other team member’s component. So people started hacking the system with hiding from their precious component related code from the browser (meaning also the browser could not do any pre-runtime CSS optimizations as well – but hey at least that dev from Tiger Team cannot break your code!).

    So if we can use a new language element to have this layer order:
    1. company global theming
    2. application specific layout
    3. component specific layout

    we are going to make the biggest complaint about CSS obsolete.

    Note, that by `component` specific layout I meant several layers named after the component. Having just one giant `component` layer would mean that that dev who learnt CSS when PHP was all the rage back in 2008 can still break my components code.

    This also means we can have hundreds of one-file scoped layers without issues.

  3. I love seeing that it’s in the works! In a way, I see myself utilizing this in my codebase, but somehow I wonder if the benefits would be felt. Meaning — what are the real use cases that this would improve?

    If you have a code structure that takes such naming conventions in mind, do you really need the layers? But since there are very few developers that can do a perfect code structure (me included for sure), then using layers would be beneficial. But! The bad practices such developers apply would just carry over to layers, no? So my wondering is — what is the real improvement over the codebase layers would provide to mid-level developers?

    Just wondering of opinions here, I don’t have a strong one yet ^^

Leave a comment

Leave a Reply to Gudang Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.