Deep Dive Into the Cascade: Use the @layer Rule to Write More Deterministic CSS

March 27, 2025

The CSS Cascade seems simple enough when you're dealing with a single stylesheet. But in real world applications, we use bundlers like webpack or vite to load thousands of files. How can we ensure that our CSS always loads in the correct order?

The @layer CSS rule allows devs to "declare a cascade layer and can also be used to define the order of precedence in case of multiple cascade layers mdn link." This is exceptionally useful for design systems maintainers who often don't control the environment in which their components are used.

If you don't have time for a deep dive, scroll down to the section called How to use @layer to ensure some styles are always overrideable. Otherwise, keep reading!

A single stylesheet

When you imagine a single stylesheet, it's pretty easy to understand the cascade. Styles are applied in the order that they are loaded, which matches the order that they are written.

Let's take a look at a code example:

If you prefer, you can play around with this code sandbox.

/* these classnames are all in the same file */ .classname-a { background-color: blue; } .classname-b { background-color: red; }
import styles from './styles.module.css'; const Component = () => { return <button className={classNames(styles["classname-a"],styles["classname-b"] )}> };

In this example, we know that the background color of the <button /> element will be red. This is because .classname-b takes precedence over classname-a in the css stylesheet since it is loaded second.

Multiple stylesheets

An example with a single stylesheet feels straightforward. Let's complicate it by adding a second stylesheet.

/* a.module.css */ .classname-a { background-color: blue; }
/* b.module.css */ .classname-b { background-color: red; }
import a from './a.module.css'; import b from './b.module.css'; const Component = () => { return <button className={classNames(a["classname-a"], b["classname-b"])}> };

What will the background color of the <button /> element be?

Still red!

We know this because we know in which order these stylesheets are loaded.

  • Since b.module.css is being loaded after a.module.css, it will take precedence.
  • If we switched the order and loaded b.module.css first, the background-color will be blue, instead.

You can play around with this in a code sandbox. Try swapping the imports in App.tsx.

OK, so we understand that the order that stylesheets are loaded matters. And we know that the last stylesheet to be loaded will always take precedence over the ones that are loaded before it.

We also know that real-world applications are far more complicated than this. We use bundlers (which are a black box to many of us) to load up hundreds of stylesheets. How can we ensure that some stylesheets always take precedence?

How to use @layer to ensure some styles are always overrideable

I was in a situation once where the cascade was working as expected, until we began to convert our internal packages from just-in-time packages to compiled packages. Vite began to resolve the modules in a different order. This affected the order of the cascade, essentially reversing it, which introduced unexpected and hard to debug visual regressions.

Luckily, I was able to use @layer to address this issue. By putting all of our design systems components on a @layer, I ensured that application styles would always take precedence, resolving all of the visual regressions.

In our simpler example, let's refactor our code to ensure that b.module.css always takes precedence:

  1. Add a global css file that gets loaded before anything else.
import "layers.css"; // make sure to load your layers file first. import b from './b.module.css'; // even though we are loading `a.module.css` second, // `b.module.css` will always take precedence because // the styles in `a.module.css` are assigned to the components layer // and the styles in `b.module.css` aren't assigned to a layer import a from './a.module.css'; const Component = () => { return <button className={classNames(a["classname-a"], b["classname-b"])}> };
  1. Define our layers in the layers.css file.
/* layers.css */ # you can name your layers whatever you want; @layer components;
  1. Put the styles in a.module.css into the appropriate layer. In our case, we only have one layer: `components``.
/* a.module.css */ @layer components { .classname-a { background-color: blue; } }
/* b.module.css */ .classname-b { background-color: red; }

The result in this case is that the button will always be red, because styles that aren't explictly on a @layer will always take precedence. It does not matter what order the stylesheets are loaded in. You can test this in this code sandbox by moving the imports around.

The @layer CSS rule allows us to write more deterministic CSS when we don't have a control over the environment our components are used in. We can always ensure that our design systems components are overrideable by application code by putting their styles on a layer.

Continue Reading

retrospective

I'm joining Honeycomb.io

I'm excited to share that after 3.5 years at Amplitude I'm joining Honeycomb in a little over a week.

Read Post