Integrating CSS Cascade Layers To An Existing Project — Smashing Magazine

Published on:

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.

(Large preview)

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:

  1. !important styles in the legacy layer (most powerful),
  2. !important styles in the new layer,
  3. Normal styles in the new layer,
  4. 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 like box-sizing, margins, and paddings.
  • base: Default styles of HTML elements, like body, 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

Related