Assessing The Styles Before Layering
Rather than setting up a synthetic tutorial project, it’s far more instructive to take an existing codebase with real quirks and see how cascade layers fit into it. The project used here is a straightforward landing page — navigation, hero section, buttons, and a mobile menu — whose styles look polished on the surface but reveal fundamental organizational problems underneath.
There are three files involved: index.html, index.css, and index.js. The CSS file exceeds 450 lines, and even a quick skim exposes several issues:
- Repetitive selectors targeting the same elements across many rule sets.
- Dozens of
#idselectors, which are generally discouraged. - Duplicate definitions of
#botLogoappearing more than 70 lines apart. - Heavy reliance on the
!importantflag to force styles.
And yet, the page displays correctly, proving that CSS tolerates interwoven specificity issues without raising errors — it simply renders whatever wins according to the cascade.
Choosing The Right Layer Breakdown
One option would be to wrap everything in a single legacy layer and leave it at that. While possible, this approach is less useful in practice. Cascade layers derive their power from order — any later-declared layer overrides earlier ones — so a single catch-all layer provides no room for future styles to interplay predictably.
There’s an additional complication: !important declarations reverse the layer priority order entirely. If a global legacy layer is followed by a new layer, the actual weight of styles in presence of !important would rank as:
!importantrules within thelegacylayer (strongest).!importantrules within thenewlayer.- Normal rules in the
newlayer. - Normal rules in the
legacylayer (weakest).
With that in mind, a five-layer split provides clear separation of concerns and predictable override behavior:
reset— resets such as box sizing, margins, padding.base— defaults for elements: body, headings, paragraphs, links.layout— page structure and positioning.components— reusable UI blocks like buttons and menus.utilities— single-purpose helper classes.
This split is one reasonable approach; other organizational schemes exist, including further subdividing components into smaller layers. However, overly granular layering adds management overhead and is best reserved for projects backed by a formal design system.
Unlayered styles, which automatically receive the highest priority, are another option worth keeping in mind. Yet, explicitly containing every rule within a layer keeps the codebase modular and easier to reason about — a worthwhile outcome for a legacy project undergoing this refactor.
Ordering the Layers First
Defining the layer order at the top of the file maps out which layer takes precedence (priority increases from left to right), so decisions shift from selector weight to layer responsibility. Working top to bottom through the stylesheet, the first clear-cut change is dropping the duplicate Poppins font @import from the CSS, since it already appears in index.html, which is the recommended way to load fonts quickly.
The universal selector styles are classic reset rules and fit naturally in the @layer reset:
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
The body selector follows, holding global styles like backgrounds and fonts, making it a solid candidate for @layer base:
@layer base {
body {
background-image: url("bg.svg"); /* Renamed to bg.svg for clarity */
font-family: "Poppins", sans-serif;
/* ... other styles */
}
}
Removing IDs From the Specificity Equation
The page loader is an ID selector (#loader) that is better handled with a class. Class selectors keep specificity low from the start and prevent the sort of conflicts that inevitably trigger specificity battles. Moving in index.html, the loader markup is refactored to class="loader", and the page-level element with id="page" gets the same treatment at the same time.
That markup pass also reveals stray div elements missing closing tags and a <script> tag buried inside the .heading element. Those get cleaned up, and the script is made a direct child of body to keep script loading straightforward.
With IDs converted to classes, the loader can sit in the components layer because a loader is a reusable piece of UI:
@layer components {
.loader {
width: 100%;
height: 100vh;
/* ... */
}
.loader .loading {
/* ... */
}
.loader .loading span {
/* ... */
}
.loader .loading span:before {
/* ... */
}
}
Giving Animations Their Own Layer
Keyframes are tricky to place. The chosen approach is to isolate them in a dedicated fifth layer and update the declared order accordingly:
@layer reset, base, layout, components, utilities, animations;
Animations are placed last because they run after other styles settle and should not be subject to style conflicts. Gathering all @keyframes rules there separates static from dynamic styles and enforces reusability:
@layer animations {
@keyframes loading {
/* ... */
}
@keyframes loading2 {
/* ... */
}
@keyframes pageShow {
/* ... */
}
}
Back to layout concerns: the .page class controls the initial visibility of content, so it is now in the layout layer:
@layer layout {
.page {
display: none;
}
}
Custom scrollbars apply across the whole site and are global defaults, so they fit best in @layer base:
@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;
}
}
The nav element is the frame that sets the navigation bar’s position and size, making it part of layout:
@layer layout {
/* ... */
nav {
display: flex;
height: 55px;
width: 100%;
padding: 0 50px; /* Consistent horizontal padding */
/* ... */
}
}
Rebuilding the Logo as a Component
Three selectors—nav .logo, .logo img, and #botLogo—are redundant. They need consolidation:
nav .logois unnecessarily specific; the logo should be reusable, so it is simplified to.logoand stripped of an!importantflag..logobecomes a Flexbox container, positioning.logo imgmore reliably than the old absolute positioning.#botLogois declared twice; merge the rules and convert to the.botLogoclass. The HTML is updated to match..logo imgtransforms into.botLogo, giving all logo instances a single base class.
The result is two visually distinct logo usages—one in the navigation, one in the hero heading. Differentiating with .heading .botLogo adds just enough specificity without worrying about fights elsewhere, and duplicated styles are cleaned up along the way. The whole block belongs in the components layer:
@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);
/* ... */
}
}
Navigation List and Buttons
This pattern converting a <ul> into a horizontal flex row is reusable navigation. The existing .mainMenu class takes the place of any nav ul selectors to reduce specificity and clarify intent:
@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;
}
}
Two classes toggle the menu between open and closed states on smaller screens. Because they are tied to the navigation component, they stay with .mainMenu in components, with selectors combined and simplified for readability:
@layer components {
/* ... */
nav:is(.openMenu, .closeMenu) {
font-size: 25px;
display: none;
cursor: pointer;
color: #fff;
}
}
Dead selectors no longer referenced in the HTML markup are removed during this pass.
Where Media Queries Belong
The open question is whether responsive rules need their own layer or should live alongside the selectors they affect. After testing, media queries are kept in the same layer as the target elements because that:
- Keeps responsive styles next to base element styles,
- Makes overrides predictable, and
- Flows well with component-based architecture.
Technique-wise the responsive logic stays together, the alternative—splitting responsive behavior into its own layer—creates too wide a gap from the base styling, and it is far too easy to update styles in one layer and miss the corresponding queries in the other. Keeping media queries in the same layer gives them equal priority with their elements. CSS nesting syntax expresses the relationship between queries and components clearly:
@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;
}
}
}
Nesting also brings child styles like nav .openMenu and nav .closeMenu into the same block:
@layer components {
nav {
&.openMenu {
display: none;
@media (max-width: 900px) {
&.openMenu {
display: block;
}
}
}
}
}
< h3>Typography, Buttons, Utilities
The .title and .subtitle classes are typography pieces and belong with their responsive variants in components:
@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;
}
}
}
Buttons share the .btn class, so they land in the same place:
@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 utilities layer is reserved for single-purpose helper classes. The .noselect rule, which disables text selection on an element, is its only occupant:
@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;
}
}
That completes the refactor—the stylesheet now uses cascade layers throughout. The project’s original code and the final version are simple to compare.
Challenges Along the Way
The layer migration is not smooth sailing. Four pain points stand out:
- Figuring out where to start. Defining layers first and setting priority levels establishes a framework for handling each style rule, which prevents second-guessing about which layer fits and avoids creating unnecessary ones.
- Browser support is real. Cascade Layers have roughly 94% support as of now; working on a site that must accommodate legacy browsers means layer support cannot be assumed.
- Media queries are unclear. Whether they should be nested inside the layers they apply to or isolated in a separate layer is the hardest call; nesting in the same layer wins because it keeps related rules close.
- Stripping out
!importantis a juggling act. Important flags invert layer priority entirely. Chipping away at these highlights how the existing architecture depends on them, forcing a balance between refactoring and not breaking the cascade.
Refactoring the author’s existing code is daunting, but the complexity comes from the existing codebase rather than the layer concept itself. Overhauling an existing method is always difficult, even when the replacement is cleaner.
Are Cascade Layers Worth the Effort?
The refactor improves the project beyond style organization. Removing unused and conflicting styles may offer performance wins; the major benefit is more maintainable CSS. It is easier to find a relevant rule, understand what it does, and know where new styles should go.
That said, cascade layers are no silver bullet. CSS remains tied to the HTML it targets where selectors and structure are intertwined. On a codebase with poor structure that suffers from heavy reliance on nested divs, the effort will require untangling markup alongside the CSS. Starting fresh may be easier on that front due to less baggage, but altering an existing site means mapping out exactly how much refactoring is needed.
For a maintainability-only upgrade, adopting cascade layers is worthwhile—the organizational improvements alone justify the rework.



