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.
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!
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.
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.
b.module.css
is being loaded after a.module.css
, it will take precedence.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?
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:
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"])}> };
layers.css
file./* layers.css */ # you can name your layers whatever you want; @layer components;
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.
I'm excited to share that after 3.5 years at Amplitude I'm joining Honeycomb in a little over a week.
A review of 2021.
Have you ever accidentally overwritten a colleague's work with a force push? Here's an easy way to recover without anyone ever having to know.