You can always get a fantastic overview of things in Stephenie Eckles’ article, “Getting Started With CSS Cascade Layers”. But let’s talk about the experience of integrating cascade layers into real-world code, the good, the bad, and the spaghetti.
I could have created a sample project for a classic walkthrough, but nah, that’s not how things work in the real world. I want to get our hands dirty, like inheriting code with styles that work and no one knows why.
Finding projects without cascade layers was easy. The tricky part was finding one that was messy enough to have specificity and organisation issues, but broad enough to illustrate different parts of cascade layers integration.
Ladies and gentlemen, I present you with this Discord bot website by Drishtant Ghosh. I’m deeply grateful to Drishtant for allowing me to use his work as an example. This project is a typical landing page with a navigation bar, a hero section, a few buttons, and a mobile menu.
You see how it looks perfect on the outside. Things get interesting, however, when we look at the CSS styles under the hood.
Understanding The Project
Before we start throwing @layers
around, let’s get a firm understanding of what we’re working with. I cloned the GitHub repo, and since our focus is working with CSS Cascade Layers, I’ll focus only on the main page, which consists of three files: index.html
, index.css
, and index.js
.
Note: I didn’t include other pages of this project as it’d make this tutorial too verbose. However, you can refactor the other pages as an experiment.
The index.css
file is over 450 lines of code, and skimming through it, I can see some red flags right off the bat:
- There’s a lot of code repetition with the same selectors pointing to the same HTML element.
- There are quite a few
#id
selectors, which one might argue shouldn’t be used in CSS (and I am one of those people). #botLogo
is defined twice and over 70 lines apart.- The
!important
keyword is used liberally throughout the code.
And yet the site works. There is nothing “technically” wrong here, which is another reason CSS is a big, beautiful monster — errors are silent!
Planning The Layer Structure
Now, some might be thinking, “Can’t we simply move all of the styles into a single layer, like @layer legacy
and call it a day?”
You could… but I don’t think you should.
Think about it: If more layers are added after the legacy
layer, they should override the styles contained in the legacy
layer because the specificity of layers is organized by priority, where the layers declared later carry higher priority.
/* new is more specific */
@layer legacy, new;
/* legacy is more specific */
@layer new, legacy;
That said, we must remember that the site’s existing styles make liberal use of the !important
keyword. And when that happens, the order of cascade layers gets reversed. So, even though the layers are outlined like this:
@layer legacy, new;
…any styles with an !important
declaration suddenly shake things up. In this case, the priority order becomes:
!important
styles in thelegacy
layer (most powerful),!important
styles in thenew
layer,- Normal styles in the
new
layer, - Normal styles in the
legacy
layer (least powerful).
I just wanted to clear that part up. Let’s continue.
We know that cascade layers handle specificity by creating an explicit order where each layer has a clear responsibility, and later layers always win.
So, I decided to split things up into five distinct layers:
reset
: Browser default resets likebox-sizing
, margins, and paddings.base
: Default styles of HTML elements, likebody
,h1
,p
,a
, etc., including default typography and colours.layout
: Major page structure stuff for controlling how elements are positioned.components
: Reusable UI segments, like buttons, cards, and menus.utilities
: Single helper modifiers that do just one thing and do it well.
This is merely how I like to break things out and organize styles. Zell Liew, for example, has a different set of four buckets that could be defined as layers.
There’s also the concept of dividing things up even further into sublayers:
@layer components {
/* sub-layers */
@layer buttons, cards, menus;
}
/* or this: */
@layer components.buttons, components.cards, components.menus;
That might come in handy, but I also don’t want to overly abstract things. That might be a better strategy for a project that’s scoped to a well-defined design system.
Another thing we could leverage is unlayered styles and the fact that any styles not contained in a cascade layer get the highest priority:
@layer legacy { a { color: red !important; } }
@layer reset { a { color: orange !important; } }
@layer base { a { color: yellow !important; } }
/* unlayered */
a { color: green !important; } /* highest priority */
But I like the idea of keeping all styles organized in explicit layers because it keeps things modular and maintainable, at least in this context.
Let’s move on to adding cascade layers to this project.
Integrating Cascade Layers
We need to define the layer order at the top of the file:
@layer reset, base, layout, components, utilities;
This makes it easy to tell which layer takes precedence over which (they get more priority from left to right), and now we can think in terms of layer responsibility instead of selector weight. Moving forward, I’ll proceed through the stylesheet from top to bottom.
First, I noticed that the Poppins font was imported in both the HTML and CSS files, so I removed the CSS import and left the one in index.html
, as that’s generally recommended for quickly loading fonts.
Next is the universal selector (*
) styles, which include classic reset styles that are perfect for @layer reset
:
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
With that out of the way, the body
selector is next. I’m putting this into @layer base
because it contains core styles for the project, like backgrounds and fonts:
@layer base {
body {
background-image: url("bg.svg"); /* Renamed to bg.svg for clarity */
font-family: "Poppins", sans-serif;
/* ... other styles */
}
}
The way I’m tackling this is that styles in the base
layer should generally affect the whole document. So far, no page breaks or anything.
Swapping IDs For Classes
Following the body
element selector is the page loader, which is defined as an ID selector, #loader
.
I’m a firm believer in using class selectors over ID selectors as much as possible. It keeps specificity low by default, which prevents specificity battles and makes the code a lot more maintainable.
So, I went into the index.html
file and refactored elements with id="loader"
to class="loader"
. In the process, I saw another element with id="page"
and changed that at the same time.
While still in the index.html
file, I noticed a few div
elements missing closing tags. It is astounding how permissive browsers are with that. Anyways, I cleaned those up and moved the
Source link