Cascade Layers (CSS Layer)
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
Taming the Cascade

  Introducing Cascade Layers
  
    Creating a Cascade Layer
    Managing Layer Order
    Layers in the Cascade
  

Intermediate Summary

  Details you need to know
  
    Unlayered Styles come first last in the Layer Order
    Naming a Layer is optional
    Layers can be nested
  


  A few more notes / caveats
  
    Cascade Layers and the use of !important
    Cascade Layers vs. Media Queries (and other conditionals)
    Cascade Layers vs. “Name-Defining Rules”
    No Interleaving of @import/@namespace and @layer
  

Browser Support
In Closing


~





# 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:

Origin and Importance
Context
Style Attribute
Specificity
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 my CSS Day 2022 talk on the subject. It also covers Cascade Layers, so you can watch the entire thing instead of continuing to read 😉You can find the the slides here.


~





# 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 — if not having thrown in an !important somewhere. 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:


Structuring your code in such a way that you create some sort of logical order that works for most scenarios.
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 Suzanne — whom 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:



Using the @layer block at-rule, with styles assigned immediately to it:
@layer reset {
  * { /* Poor Man's Reset */
    margin: 0;
    padding: 0;
  }
}


Using the @layer statement at-rule, without any styles assigned:
@layer reset;


Using @import with the layer keyword or layer() function:
@import url(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:


reset
base
theme
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):


Origin and Importance
Context
Style Attribute
Layers
Specificity
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:


reset
base
theme
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 url(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 url(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:


reset
base
theme
utilities
(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”.



Using the @layer block at-rule, with styles assigned immediately to it
@layer {
  * { /* Poor Man's Reset */
    margin: 0;
    padding: 0;
  }
}


Using @import:
@import url(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:


base
framework


The framework layer itself also contains two layers:


base
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:


base

  framework
  
    base
    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:


base
framework.base
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):


Transitions
Important User-Agent
Important User
Important Author
Animations
Normal Author
Normal User
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:


  Normal reset Layer
  Normal base Layer
  Normal theme Layer
  Normal utilities Layer


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


  Important utilities Layer
  Important theme Layer
  Important base Layer
  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:


framework
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:

Establish a layer order upfront using @layer statement at-rules
Group your @imports after that
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 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)✅ Supported in Chromium 99 and upWas supported as an Experimental Feature in Chrome 96+ through the --enable-blink-features=CSSCascadeLayers run-time flag. Starting with version 96.0.4661.0, you could toggle it via the #enable-cascade-layers feature flag in chrome://flags/
Firefox (Gecko)✅ Supported in Firefox 97 and upWas supported as an Experimental Feature in Firefox 94+ (current Canary) by setting layout.css.cascade-layers.enabled to true via about:config.
Safari (WebKit)✅ Supported in Safari 15.4 and upWas supported as an Experimental Feature in Safari Technology Preview 133. Had to be enabled manually by using 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:


Chromium: Issue #1095765
Firefox: Issue #1699215
Safari: Issue #220779


~






# 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:

— Bram.us (@bramusblog) September 15, 2021 

~




	