iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🙌

Time to Dive into CSS Cascade Layers (@layer)

に公開
2023/02/03

Even though cascade layers @layer are supported in modern browsers, I thought that seeing them in actual production was still a story for the future due to compatibility issues. However, when I checked Can I use... Support tables for HTML5, CSS3, etc on a whim, I found that as of now (2022.12.27), they already have nearly 90% global support. Since I expect to see them more often starting next year, I think it's about time I face cascade layers.

https://caniuse.com/css-cascade-layers

Cascade Layers @layer

Cascade layers are one of the algorithms used to control style priority. By using cascade layers, you can manage style priority by layering (stratifying) them. The most important thing to remember is that the cascade algorithm takes precedence over the specificity algorithm. In other words, even if a style loses in terms of specificity, it may still be applied preferentially depending on the cascade layer.

Since it's hard to understand with just words, let's look at a simple example.

html
<p id="hoge" class="hoge">piyo</p>
css
/* #hoge { color: red } takes precedence in specificity comparison */
#hoge { color: red }
.hoge { color: blue }

Suppose you have the HTML and CSS descriptions as shown above. In the specificity comparison, the ID selector takes precedence over the class selector, so color: red is applied in this case.

Next, let's look at an example that introduces cascade layers.

css
/*
  First, declare the order of the layers.
  In this case, overwrite takes precedence.
*/
@layer base, overwrite;
@layer base {
  #hoge { color: red }
}
@layer overwrite {
  .hoge { color: blue }
}

Layers are declared using @layer. Since the cascade layer algorithm takes precedence over specificity, in the above case, the color: blue from the overwrite layer is applied.

By introducing cascade layers in this way, you can manage style priorities on a different axis from traditional CSS design that relied on specificity.

Basic Syntax

There are three ways to declare cascade layers:

  • Declaration using block syntax and @layer
  • Declaration using statement syntax and @layer
  • Declaration using the layer() function or layer keyword with @import

Let's check them one by one.

Declaration using block syntax and @layer

First, let's look at declaration using block syntax and @layer.

Block syntax
@layer layer-name {
  /* Style descriptions */
}

Declare the name of the layer with @layer and write the styles inside the block {}. In the following example, the layer written later takes precedence regardless of specificity (see Layer Order).

html
<p class="hoge">fuga</p>
css
/* The 'overwrite' layer written later takes precedence */
@layer base {
  .hoge { color: red; }
}
@layer overwrite {
  p { color: blue }
}

You can also declare an anonymous layer without providing a layer name.

css
@layer {
  .hoge { color: red }
}

Declaration using statement syntax and @layer

Next, let's look at the statement syntax.

Statement syntax
@layer layer-name-1, layer-name-2;

In statement syntax, you can declare layers without any styles. Unlike block syntax, this only involves the priority of the layers (see Layer Order).

css
/* 'overwrite' takes precedence due to the statement syntax */
@layer base, overwrite;
@layer overwrite {
  p { color: blue }
}
@layer base {
  .hoge { color: red }
}

Declaration using the layer() function or layer keyword with @import

You can also declare layers for CSS imported via @import. In this case, instead of @layer, you use either the layer() function or the layer keyword.

@import and layer() function
@import url("**/*.css") layer(layer-name);

When using the layer() function as shown above, you can pass the name of the layer to be declared as an argument.

css
/* 'base' and 'custom' layers are declared */
@import url("base.css") layer(base);
@import url("custom.css") layer(custom);

On the other hand, if you declare it with the layer keyword as follows, it will be declared as an anonymous layer.

@import and layer keyword
/* This becomes an anonymous layer */
@import url("**/*.css") layer;

Layer Order

Now that we understand how to declare layers, let's look at the order of layers. In cascade layers, layers are sorted in the order they are first declared. In this process, the later layer takes higher precedence.

css
.hoge { color: red }
@layer reset {
  .hoge { color: green }
}
@layer base {
  .hoge { color: blue }
}
@layer overwrite {
  .hoge { color: yellow }
}

For example, in the case above, each layer is sorted in the following order:

  1. reset layer
  2. base layer
  3. overwrite layer
  4. (Implicit outer layer)

Since the later layer has higher priority, the layer with the highest priority among these is the (implicit outer layer), and the one with the lowest priority is reset.

Here, the term "implicit outer layer" has appeared. The implicit outer layer refers to styles outside of layers explicitly declared using @layer, the layer() function, or the layer keyword. In the example above, it refers to the .hoge { color: red } part, which can also be described as unlayered styles.

This implicit outer layer is (implicitly) added last in the layer order, making its priority higher than that of explicitly declared layers. Note that in the code above, color: red will be applied.

The fact that each layer is sorted in the order of its first declaration remains the same even with statement syntax. For example, in the following code, the priority is determined by the statement syntax.

css
/* Priority is predetermined by the statement syntax */
@layer overwrite, base, reset;

.hoge { color: red }
@layer reset {
  .hoge { color: green }
}
@layer base {
  .hoge { color: blue }
}
@layer overwrite {
  .hoge { color: yellow }
}

In the above case, the layers are sorted in the following order:

  1. overwrite layer
  2. base layer
  3. reset layer
  4. (Implicit outer layer)

Please be aware that the priority of the (implicit outer layer) remains the highest. The order of reset, base, and overwrite can be easily rearranged using statement syntax.

Nested Layers

Layers can be nested.

css
@layer framework {
  .hoge { color: red }
  @layer base {
    .hoge { color: green }
  }
  @layer custom {
    .hoge { color: blue }
  }
}

When layers are nested, the deeper the nesting, the lower the priority. In the example above, the layer order is sorted as follows (note that the later layer has higher priority):

  1. framework.base layer
  2. framework.custom layer
  3. framework layer
  4. (Implicit outer layer)

In this case, since there is no style for the (implicit outer layer), the framework layer's style .hoge { color: red } will take precedence.

Additionally, nested layers can also be declared using period (.) notation, as shown in the following example.

css
@layer framework.base {
  .hoge { color: green }
}
@layer framework.custom {
  .hoge { color: blue }
}
@layer framework {
  .hoge { color: red }
}
/* Equivalent to the following:
@layer framework {
  .hoge { color: red }
  @layer base {
    .hoge { color: green }
  }
  @layer custom {
    .hoge { color: blue }
  }
}
*/

Adding Styles to Existing Layers

If you declare multiple layers with the same name, styles in the later declarations are added to the existing layer. In the following example, color: red is applied.

html
<p class="hoge">fuga</p>
css
@layer base {
  .hoge { color: red }
}
@layer base {
  p { color: blue }
}
/* Equivalent to the following, so `color: red` takes precedence:
@layer base {
  .hoge { color: red }
  p { color: blue }
}
*/

Since each layer is sorted in the order it was first declared, in the following example, the overwrite layer takes precedence over the base layer.

html
<p id="hoge" class="fuga">piyo</p>
css
@layer base {
  p { color: green }
}
@layer overwrite {
  .fuga { color: red }
}
@layer base {
  #hoge { color: blue }
}
/* Equivalent to the following, so the `color: red` from the overwrite layer takes precedence:
@layer base {
  p { color: green }
  #hoge { color: blue }
}
@layer overwrite {
  .fuga { color: red }
}
*/

Anonymous Layers

As mentioned several times in this article, you can declare anonymous layers in cascade layers. Please note that an anonymous layer is an explicit layer declared without a name, and is distinct from the implicit outer layer.

css
/* Declaration of an anonymous layer via @import */
@import url("base.css") layer;

/* Declaration of an anonymous layer via block syntax */
@layer {
  .hoge {
    color: red;
  }
}

Anonymous layers create separate layers for each declaration

Anonymous layers create a new, separate layer for each declaration. In the following code, the base layer is described in two separate block syntaxes. In this case, the styles in the second block syntax are added to the existing layer (see Adding Styles to Existing Layers).

css
@layer base {
  .hoge { color: red }
}
@layer base {
  p { color: blue }
}
/* Equivalent to the following:
@layer base {
  .hoge { color: red }
  p { color: blue }
}
*/

However, in the case of anonymous layers, they are treated as separate, independent layers.

css
/* These are treated as separate layers */
@layer {
  .hoge { color: red }
}
@layer {
  p { color: blue }
}
/* This is NOT equivalent to the following!!
@layer {
  .hoge { color: red }
  p { color: blue }
}
*/

In descriptions like the following, be aware that the behavior may differ between named layers and anonymous layers.

css
/* In this case, `color: blue` takes precedence */
@layer base {
  .hoge { color: red }
}
@layer custom {
  .hoge { color: blue }
}
@layer base {
  .hoge { color: green }
}

/* In this case, `color: green` takes precedence */
@layer {
  .hoge { color: red }
}
@layer custom {
  .hoge { color: blue }
}
@layer {
  .hoge { color: green }
}

Anonymous layers cannot be referenced from the outside

Anonymous layers do not provide a way to overwrite or add styles to that layer from the outside. While this is just one example, it can be effective in cases such as:

  • When you want to restrict the styles of a specific layer to a single location to enforce code conventions in team development.
  • When you want to prevent CSS provided as a library from being manipulated from the outside.
css
/*
  external.css
  Declare several CSS files as anonymous layers
*/
@import url(external-reset.css) layer;
@import url(external-base.css) layer;
@import url(external-utility.css) layer;

/*
  Import `external.css` from another CSS file
  Modifications cannot be made to the internal anonymous layers
*/
@import url(external.css) layer(external);
@layer external {
  /* ... */
}

Comparison with Inline Styles

When comparing layered styles and inline styles, the inline style takes precedence. In the following example, color: red is applied.

html
<!-- `color: red` is applied -->
<p id="hoge" class="fuga" style="color: red">piyo</p>
css
@layer {
  #hoge.fuga { color: blue }
}

Comparison with !important

When comparing layered styles and styles with !important, the style with !important takes precedence.

css
.hoge { color: red }
@layer base {
  /* This takes precedence */
  .hoge { color: green !important }
}
@layer overwrite {
  .hoge { color: blue }
}

For example, in the case above, each layer is sorted in the following order:

  1. base layer
  2. overwrite layer
  3. (Implicit outer layer)

However, since a style with !important has higher precedence than cascade layers, color: green !important will be prioritized in the above example.

Comparison between !important styles reverses cascade layer priority

This is a behavior that you might accidentally get tripped up by, but in a comparison between !important styles, the priority of cascade layers is reversed.

Let's look at an example to see it in action. Confusingly, in the following example, color: green !important is applied.

html
<p class="hoge">fuga</p>
css
@layer base, overwrite;
.hoge { color: red !important }
@layer base {
  /* This description is applied!! */
  .hoge { color: green !important }
}
@layer overwrite {
  .hoge { color: blue !important }
}

Let me briefly explain what is happening. First, forget about !important for a moment and check the order of layers in the code above.

  1. base layer
  2. overwrite layer
  3. (Implicit outer layer)

This is what we have seen several times in this article, and there is no problem here. Normally, the implicit outer layer takes precedence, but in a comparison between !important styles, the layer priority is reversed. In other words, the layer order is sorted as follows:

  1. (Implicit outer layer) - !important
  2. overwrite layer - !important
  3. base layer - !important

Therefore, the !important in the base layer has the highest priority, and color: green !important is applied in the above case.

Conclusion

What did you think? This was a look at facing cascade layers. Since there were some unexpectedly confusing aspects, I felt it is necessary to organize your knowledge beforehand when introducing them. While cascade layers are convenient, their scope of influence is also large, so it remains important to think carefully about CSS design.

References

https://www.w3.org/TR/css-cascade-5/#layering
https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
https://css-tricks.com/css-cascade-layers/

Discussion