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 ProjectBefore 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 StructureNow, 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 the legacy layer (most powerful),!important styles in the new 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 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 LayersWe 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 ClassesFollowing 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 tag out of the .heading element to be a direct child of body. Let’s not make it any tougher to load our scripts.Now that we’ve levelled the specificity playing field by moving IDs to classes, we can drop them into the components layer since a loader is indeed a reusable component:@layer components { .loader { width: 100%; height: 100vh; /* ... */ } .loader .loading { /* ... */ } .loader .loading span { /* ... */ } .loader .loading span:before { /* ... */ }}AnimationsNext are keyframes, and this was a bit tricky, but I eventually chose to isolate animations in their own new fifth layer and updated the layer order to include it:@layer reset, base, layout, components, utilities, animations;But why place animations as the last layer? Because animations are generally the last to run and shouldn’t be affected by style conflicts.I searched the project’s styles for @keyframes and dumped them into the new layer:@layer animations { @keyframes loading { /* ... */ } @keyframes loading2 { /* ... */ } @keyframes pageShow { /* ... */ }}This gives a clear distinction of static styles from dynamic ones while also enforcing reusability.LayoutsThe #page selector also has the same issue as #id, and since we fixed it in the HTML earlier, we can modify it to .page and drop it in the layout layer, as its main purpose is to control the initial visibility of the content:@layer layout { .page { display: none; }}Custom ScrollbarsWhere do we put these? Scrollbars are global elements that persist across the site. This might be a gray area, but I’d say it fits perfectly in @layer base since it’s a global, default feature.@layer base { /* ... */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #0e0e0f; } ::-webkit-scrollbar-thumb { background: #5865f2; border-radius: 100px; } ::-webkit-scrollbar-thumb:hover { background: #202225; }}I also removed the !important keywords as I came across them.NavigationThe nav element is pretty straightforward, as it is the main structure container that defines the position and dimensions of the navigation bar. It should definitely go in the layout layer:@layer layout { /* ... */ nav { display: flex; height: 55px; width: 100%; padding: 0 50px; /* Consistent horizontal padding */ /* ... */ }}LogoWe have three style blocks that are tied to the logo: nav .logo, .logo img, and #botLogo. These names are redundant and could benefit from inheritance component reusability.Here’s how I’m approaching it:The nav .logo is overly specific since the logo can be reused in other places. I dropped the nav so that the selector is just .logo. There was also an !important keyword in there, so I removed it.I updated .logo to be a Flexbox container to help position .logo img, which was previously set with less flexible absolute positioning.The #botLogo ID is declared twice, so I merged the two rulesets into one and lowered its specificity by making it a .botLogo class. And, of course, I updated the HTML to replace the ID with the class.The .logo img selector becomes .botLogo, making it the base class for styling all instances of the logo.Now, we’re left with this:/* initially .logo img */.botLogo { border-radius: 50%; height: 40px; border: 2px solid #5865f2;}/* initially #botLogo */.botLogo { border-radius: 50%; width: 180px; /* ... */}The difference is that one is used in the navigation and the other in the hero section heading. We can transform the second .botLogo by slightly increasing the specificity with a .heading .botLogo selector. We may as well clean up any duplicated styles as we go.Let’s place the entire code in the components layer as we’ve successfully turned the logo into a reusable component:@layer components { /* ... */ .logo { font-size: 30px; font-weight: bold; color: #fff; display: flex; align-items: center; gap: 10px; } .botLogo { aspect-ratio: 1; /* maintains square dimensions with width */ border-radius: 50%; width: 40px; border: 2px solid #5865f2; } .heading .botLogo { width: 180px; height: 180px; background-color: #5865f2; box-shadow: 0px 0px 8px 2px rgba(88, 101, 242, 0.5); /* ... */ }}This was a bit of work! But now the logo is properly set up as a component that fits perfectly in the new layer architecture.Navigation ListThis is a typical navigation pattern. Take an unordered list () and turn it into a flexible container that displays all of the list items horizontally on the same row (with wrapping allowed). It’s a type of navigation that can be reused, which belongs in the components layer. But there’s a little refactoring to do before we add it.There’s already a .mainMenu class, so let’s lean into that. We’ll swap out any nav ul selectors with that class. Again, it keeps specificity low while making it clearer what that element does.@layer components { /* ... */ .mainMenu { display: flex; flex-wrap: wrap; list-style: none; } .mainMenu li { margin: 0 4px; } .mainMenu li a { color: #fff; text-decoration: none; font-size: 16px; /* ... */ } .mainMenu li a:where(.active, .hover) { color: #fff; background: #1d1e21; } .mainMenu li a.active:hover { background-color: #5865f2; }}There are also two buttons in the code that are used to toggle the navigation between “open” and “closed” states when the navigation is collapsed on smaller screens. It’s tied specifically to the .mainMenu component, so we’ll keep everything together in the components layer. We can combine and simplify the selectors in the process for cleaner, more readable styles:@layer components { /* ... */ nav:is(.openMenu, .closeMenu) { font-size: 25px; display: none; cursor: pointer; color: #fff; }}I also noticed that several other selectors in the CSS were not used anywhere in the HTML. So, I removed those styles to keep things trim. There are automated ways to go about this, too.Media QueriesShould media queries have a dedicated layer (@layer responsive), or should they be in the same layer as their target elements? I really struggled with that question while refactoring the styles for this project. I did some research and testing, and my verdict is the latter, that media queries ought to be in the same layer as the elements they affect.My reasoning is that keeping them together:Maintains responsive styles with their base element styles,Makes overrides predictable, andFlows well with component-based architecture common in modern web development.However, it also means responsive logic is scattered across layers. But it beats the one with a gap between the layer where elements are styled and the layer where their responsive behaviors are managed. That’s a deal-breaker for me because it’s way too easy to update styles in one layer and forget to update their corresponding responsive style in the responsive layer.The other big point is that media queries in the same layer have the same priority as their elements. This is consistent with my overall goal of keeping the CSS Cascade simple and predictable, free of style conflicts.Plus, the CSS nesting syntax makes the relationship between media queries and elements super clear. Here’s an abbreviated example of how things look when we nest media queries in the components layer:@layer components { .mainMenu { display: flex; flex-wrap: wrap; list-style: none; } @media (max-width: 900px) { .mainMenu { width: 100%; text-align: center; height: 100vh; display: none; } }}This also allows me to nest a component’s child element styles (e.g., nav .openMenu and nav .closeMenu).@layer components { nav { &.openMenu { display: none; @media (max-width: 900px) { &.openMenu { display: block; } } } }}Typography & ButtonsThe .title and .subtitle can be seen as typography components, so they and their responsive associates go into — you guessed it — the components layer:@layer components { .title { font-size: 40px; font-weight: 700; /* etc. */ } .subtitle { color: rgba(255, 255, 255, 0.75); font-size: 15px; /* etc.. */ } @media (max-width: 420px) { .title { font-size: 30px; } .subtitle { font-size: 12px; } }}What about buttons? Like many website’s this one has a class, .btn, for that component, so we can chuck those in there as well:@layer components { .btn { color: #fff; background-color: #1d1e21; font-size: 18px; /* etc. */ } .btn-primary { background-color: #5865f2; } .btn-secondary { transition: all 0.3s ease-in-out; } .btn-primary:hover { background-color: #5865f2; box-shadow: 0px 0px 8px 2px rgba(88, 101, 242, 0.5); /* etc. */ } .btn-secondary:hover { background-color: #1d1e21; background-color: rgba(88, 101, 242, 0.7); } @media (max-width: 420px) { .btn { font-size: 14px; margin: 2px; padding: 8px 13px; } } @media (max-width: 335px) { .btn { display: flex; flex-direction: column; } }}The Final LayerWe haven’t touched the utilities layer yet! I’ve reserved this layer for helper classes that are designed for specific purposes, like hiding content — or, in this case, there’s a .noselect class that fits right in. It has a single reusable purpose: to disable selection on an element.So, that’s going to be the only style rule in our utilities layer:@layer utilities { .noselect { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -webkit-user-drag: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }}And that’s it! We’ve completely refactored the CSS of a real-world project to use CSS Cascade Layers. You can compare where we started with the final code.It Wasn’t All EasyThat’s not to say that working with Cascade Layers was challenging, but there were some sticky points in the process that forced me to pause and carefully think through what I was doing.I kept some notes as I worked:It’s tough to determine where to start with an existing project.However, by defining the layers first and setting their priority levels, I had a framework for deciding how and where to move specific styles, even though I was not totally familiar with the existing CSS. That helped me avoid situations where I might second-guess myself or define extra, unnecessary layers.Browser support is still a thing!I mean, Cascade Layers enjoy 94% support coverage as I’m writing this, but you might be one of those sites that needs to accommodate legacy browsers that are unable to support layered styles.It wasn’t clear where media queries fit into the process.Media queries put me on the spot to find where they work best: nested in the same layers as their selectors, or in a completely separate layer? I went with the former, as you know.The !important keyword is a juggling act.They invert the entire layering priority system, and this project was littered with instances. Once you start chipping away at those, the existing CSS architecture erodes and requires a balance between refactoring the code and fixing what’s already there to know exactly how styles cascade.Overall, refactoring a codebase for CSS Cascade Layers is a bit daunting at first glance. The important thing, though, is to acknowledge that it isn’t really the layers that complicate things, but the existing codebase.It’s tough to completely overhaul someone’s existing approach for a new one, even if the new approach is elegant.Where Cascade Layers Helped (And Didn’t)Establishing layers improved the code, no doubt. I’m sure there are some performance benchmarks in there since we were able to remove unused and conflicting styles, but the real win is in a more maintainable set of styles. It’s easier to find what you need, know what specific style rules are doing, and where to insert new styles moving forward.At the same time, I wouldn’t say that Cascade Layers are a silver bullet solution. Remember, CSS is intrinsically tied to the HTML structure it queries. If the HTML you’re working with is unstructured and suffers from div-itus, then you can safely bet that the effort to untangle that mess is higher and involves rewriting markup at the same time.Refactoring CSS for cascade layers is most certainly worth the maintenance enhancements alone.It may be “easier” to start from scratch and define layers as you work from the ground up because there’s less inherited overhead and technical debt to sort through. But if you have to start from an existing codebase, you might need to de-tangle the complexity of your styles first to determine exactly how much refactoring you’re looking at.