Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Sunshine And March Vibes (2025 Wallpapers Edition)

      May 30, 2025

      The Case For Minimal WordPress Setups: A Contrarian View On Theme Frameworks

      May 30, 2025

      How To Fix Largest Contentful Paint Issues With Subpart Analysis

      May 30, 2025

      How To Prevent WordPress SQL Injection Attacks

      May 30, 2025

      Does Elden Ring Nightreign have crossplay or cross-platform play?

      May 30, 2025

      Cyberpunk 2077 sequel enters pre-production as Phantom Liberty crosses 10 million copies sold

      May 30, 2025

      EA has canceled yet another game, shuttered its developer, and started more layoffs

      May 30, 2025

      The Witcher 3: Wild Hunt reaches 60 million copies sold as work continues on The Witcher 4

      May 30, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      How Remix is shaking things up

      May 30, 2025
      Recent

      How Remix is shaking things up

      May 30, 2025

      Perficient at Kscope25: Let’s Meet in Texas!

      May 30, 2025

      Salesforce + Informatica: What It Means for Data Cloud and Our Customers

      May 30, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Does Elden Ring Nightreign have crossplay or cross-platform play?

      May 30, 2025
      Recent

      Does Elden Ring Nightreign have crossplay or cross-platform play?

      May 30, 2025

      Cyberpunk 2077 sequel enters pre-production as Phantom Liberty crosses 10 million copies sold

      May 30, 2025

      EA has canceled yet another game, shuttered its developer, and started more layoffs

      May 30, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»News & Updates»Organizing Design System Component Patterns With CSS Cascade Layers

    Organizing Design System Component Patterns With CSS Cascade Layers

    February 10, 2025

    I’m trying to come up with ways to make components more customizable, more efficient, and easier to use and understand, and I want to describe a pattern I’ve been leaning into using CSS Cascade Layers.

    I enjoy organizing code and find cascade layers a fantastic way to organize code explicitly as the cascade looks at it. The neat part is, that as much as it helps with “top-level” organization, cascade layers can be nested, which allows us to author more precise styles based on the cascade.

    The only downside here is your imagination, nothing stops us from over-engineering CSS. And to be clear, you may very well consider what I’m about to show you as a form of over-engineering. I think I’ve found a balance though, keeping things simple yet organized, and I’d like to share my findings.

    The anatomy of a CSS component pattern

    Let’s explore a pattern for writing components in CSS using a button as an example. Buttons are one of the more popular components found in just about every component library. There’s good reason for that popularity because buttons can be used for a variety of use cases, including:

    • performing actions, like opening a drawer,
    • navigating to different sections of the UI, and
    • holding some form of state, such as focus or hover.

    And buttons come in several different flavors of markup, like <button>, input[type="button"], and <a class="button">. There are even more ways to make buttons than that, if you can believe it.

    On top of that, different buttons perform different functions and are often styled accordingly so that a button for one type of action is distinguished from another. Buttons also respond to state changes, such as when they are hovered, active, and focused. If you have ever written CSS with the BEM syntax, we can sort of think along those lines within the context of cascade layers.

    .button {}
    .button-primary {}
    .button-secondary {}
    .button-warning {}
    /* etc. */

    Okay, now, let’s write some code. Specifically, let’s create a few different types of buttons. We’ll start with a .button class that we can set on any element that we want to be styled as, well, a button! We already know that buttons come in different flavors of markup, so a generic .button class is the most reusable and extensible way to select one or all of them.

    .button {
      /* Styles common to all buttons */
    }

    Using a cascade layer

    This is where we can insert our very first cascade layer! Remember, the reason we want a cascade layer in the first place is that it allows us to set the CSS Cascade’s reading order when evaluating our styles. We can tell CSS to evaluate one layer first, followed by another layer, then another — all according to the order we want. This is an incredible feature that grants us superpower control over which styles “win” when applied by the browser.

    We’ll call this layer components because, well, buttons are a type of component. What I like about this naming is that it is generic enough to support other components in the future as we decide to expand our design system. It scales with us while maintaining a nice separation of concerns with other styles we write down the road that maybe aren’t specific to components.

    /* Components top-level layer */
    @layer components {
      .button {
        /* Styles common to all buttons */
      }
    }

    Nesting cascade layers

    Here is where things get a little weird. Did you know you can nest cascade layers inside classes? That’s totally a thing. So, check this out, we can introduce a new layer inside the .button class that’s already inside its own layer. Here’s what I mean:

    /* Components top-level layer */
    @layer components {
    
      .button {
        /* Component elements layer */
        @layer elements {
          /* Styles */
        }
      }
    }

    This is how the browser interprets that layer within a layer at the end of the day:

    @layer components {
      @layer elements {
        .button {
          /* button styles... */
        }
      }
    }

    This isn’t a post just on nesting styles, so I’ll just say that your mileage may vary when you do it. Check out Andy Bell’s recent article about using caution with nested styles.

    Structuring styles

    So far, we’ve established a .button class inside of a cascade layer that’s designed to hold any type of component in our design system. Inside that .button is another cascade layer, this one for selecting the different types of buttons we might encounter in the markup. We talked earlier about buttons being <button>, <input>, or <a> and this is how we can individually select style each type.

    We can use the :is() pseudo-selector function as that is akin to saying, “If this .button is an <a> element, then apply these styles.”

    /* Components top-level layer */
    @layer components {
      .button {
        /* Component elements layer */
        @layer elements {
          /* styles common to all buttons */
    
          &:is(a) {
            /* <a> specific styles */
          }
    
          &:is(button) {
            /* <button> specific styles */
          }
    
          /* etc. */
        }
      }
    }

    Defining default button styles

    I’m going to fill in our code with the common styles that apply to all buttons. These styles sit at the top of the elements layer so that they are applied to any and all buttons, regardless of the markup. Consider them default button styles, so to speak.

    /* Components top-level layer */
    @layer components {
      .button {
        /* Component elements layer */
        @layer elements {
          background-color: darkslateblue;
          border: 0;
          color: white;
          cursor: pointer;
          display: grid;
          font-size: 1rem;
          font-family: inherit;
          line-height: 1;
          margin: 0;
          padding-block: 0.65rem;
          padding-inline: 1rem;
          place-content: center;
          width: fit-content;
        }
      }
    }

    Defining button state styles

    What should our default buttons do when they are hovered, clicked, or in focus? These are the different states that the button might take when the user interacts with them, and we need to style those accordingly.

    I’m going to create a new cascade sub-layer directly under the elements sub-layer called, creatively, states:

    /* Components top-level layer */
    @layer components {
      .button {
        /* Component elements layer */
        @layer elements {
          /* Styles common to all buttons */
        }
    
        /* Component states layer */
        @layer states {
          /* Styles for specific button states */
        }
      }
    }

    Pause and reflect here. What states should we target? What do we want to change for each of these states?

    Some states may share similar property changes, such as :hover and :focus having the same background color. Luckily, CSS gives us the tools we need to tackle such problems, using the :where() function to group property changes based on the state. Why :where() instead of :is()? :where() comes with zero specificity, meaning it’s a lot easier to override than :is(), which takes the specificity of the element with the highest specificity score in its arguments. Maintaining low specificity is a virtue when it comes to writing scalable, maintainable CSS.

    /* Component states layer */
    @layer states {
      &:where(:hover, :focus-visible) {
        /* button hover and focus state styles */
      }
    }

    But how do we update the button’s styles in a meaningful way? What I mean by that is how do we make sure that the button looks like it’s hovered or in focus? We could just slap a new background color on it, but ideally, the color should be related to the background-color set in the elements layer.

    So, let’s refactor things a bit. Earlier, I set the .button element’s background-color to darkslateblue. I want to reuse that color, so it behooves us to make that into a CSS variable so we can update it once and have it apply everywhere. Relying on variables is yet another virtue of writing scalable and maintainable CSS.

    I’ll create a new variable called --button-background-color that is initially set to darkslateblue and then set it on the default button styles:

    /* Component elements layer */
    @layer elements {
      --button-background-color: darkslateblue;
    
      background-color: var(--button-background-color);
      border: 0;
      color: white;
      cursor: pointer;
      display: grid;
      font-size: 1rem;
      font-family: inherit;
      line-height: 1;
      margin: 0;
      padding-block: 0.65rem;
      padding-inline: 1rem;
      place-content: center;
      width: fit-content;
    }

    Now that we have a color stored in a variable, we can set that same variable on the button’s hovered and focused states in our other layer, using the relatively new color-mix() function to convert darkslateblue to a lighter color when the button is hovered or in focus.

    Back to our states layer! We’ll first mix the color in a new CSS variable called --state-background-color:

    /* Component states layer */
    @layer states {
      &:where(:hover, :focus-visible) {
        /* custom property only used in state */
        --state-background-color: color-mix(
          in srgb, 
          var(--button-background-color), 
          white 10%
        );
      }
    }

    We can then apply that color as the background color by updating the background-color property.

    /* Component states layer */
    @layer states {
      &:where(:hover, :focus-visible) {
        /* custom property only used in state */
        --state-background-color: color-mix(
          in srgb, 
          var(--button-background-color), 
          white 10%
        );
    
        /* applying the state background-color */
        background-color: var(--state-background-color);
      }
    }

    Defining modified button styles

    Along with elements and states layers, you may be looking for some sort of variation in your components, such as modifiers. That’s because not all buttons are going to look like your default button. You might want one with a green background color for the user to confirm a decision. Or perhaps you want a red one to indicate danger when clicked. So, we can take our existing default button styles and modify them for those specific use cases

    If we think about the order of the cascade — always flowing from top to bottom — we don’t want the modified styles to affect the styles in the states layer we just made. So, let’s add a new modifiers layer in between elements and states:

    /* Components top-level layer */
    @layer components {
    
      .button {
      /* Component elements layer */
      @layer elements {
        /* etc. */
      }
    
      /* Component modifiers layer */
      @layer modifiers {
        /* new layer! */
      }
    
      /* Component states layer */
      @layer states {
        /* etc. */
      }
    }

    Similar to how we handled states, we can now update the --button-background-color variable for each button modifier. We could modify the styles further, of course, but we’re keeping things fairly straightforward to demonstrate how this system works.

    We’ll create a new class that modifies the background-color of the default button from darkslateblue to darkgreen. Again, we can rely on the :is() selector because we want the added specificity in this case. That way, we override the default button style with the modifier class. We’ll call this class .success (green is a “successful” color) and feed it to :is():

    /* Component modifiers layer */
    @layer modifiers {
      &:is(.success) {
        --button-background-color: darkgreen;
      }
    }

    If we add the .success class to one of our buttons, it becomes darkgreen instead darkslateblue which is exactly what we want. And since we already do some color-mix()-ing in the states layer, we’ll automatically inherit those hover and focus styles, meaning darkgreen is lightened in those states.

    /* Components top-level layer */
    @layer components {
      .button {
        /* Component elements layer */
        @layer elements {
          --button-background-color: darkslateblue;
    
          background-color: var(--button-background-color);
          /* etc. */
    
        /* Component modifiers layer */
        @layer modifiers {
          &:is(.success) {
            --button-background-color: darkgreen;
          }
        }
    
        /* Component states layer */
        @layer states {
          &:where(:hover, :focus) {
            --state-background-color: color-mix(
              in srgb,
              var(--button-background-color),
              white 10%
            );
    
            background-color: var(--state-background-color);
          }
        }
      }
    }

    Putting it all together

    We can refactor any CSS property we need to modify into a CSS custom property, which gives us a lot of room for customization.

    /* Components top-level layer */
    @layer components {
      .button {
        /* Component elements layer */
        @layer elements {
          --button-background-color: darkslateblue;
    
          --button-border-width: 1px;
          --button-border-style: solid;
          --button-border-color: transparent;
          --button-border-radius: 0.65rem;
    
          --button-text-color: white;
    
          --button-padding-inline: 1rem;
          --button-padding-block: 0.65rem;
    
          background-color: var(--button-background-color);
          border: 
            var(--button-border-width) 
            var(--button-border-style) 
            var(--button-border-color);
          border-radius: var(--button-border-radius);
          color: var(--button-text-color);
          cursor: pointer;
          display: grid;
          font-size: 1rem;
          font-family: inherit;
          line-height: 1;
          margin: 0;
          padding-block: var(--button-padding-block);
          padding-inline: var(--button-padding-inline);
          place-content: center;
          width: fit-content;
        }
    
        /* Component modifiers layer */
        @layer modifiers {
          &:is(.success) {
            --button-background-color: darkgreen;
          }
    
          &:is(.ghost) {
            --button-background-color: transparent;
            --button-text-color: black;
            --button-border-color: darkslategray;
            --button-border-width: 3px;
          }
        }
    
        /* Component states layer */
        @layer states {
          &:where(:hover, :focus) {
            --state-background-color: color-mix(
              in srgb,
              var(--button-background-color),
              white 10%
            );
    
            background-color: var(--state-background-color);
          }
        }
      }
    }
    CodePen Embed Fallback

    P.S. Look closer at that demo and check out how I’m adjusting the button’s background using light-dark() — then go read Sara Joy’s “Come to the light-dark() Side” for a thorough rundown of how that works!


    What do you think? Is this something you would use to organize your styles? I can see how creating a system of cascade layers could be overkill for a small project with few components. But even a little toe-dipping into things like we just did illustrates how much power we have when it comes to managing — and even taming — the CSS Cascade. Buttons are deceptively complex but we saw how few styles it takes to handle everything from the default styles to writing the styles for their states and modified versions.


    Organizing Design System Component Patterns With CSS Cascade Layers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    Source: Read More 

    Hostinger
    Facebook Twitter Reddit Email Copy Link
    Previous ArticleMake Any File a Template Using This Hidden macOS Tool
    Next Article Creating My First Game Prototype in a Browser: The Journey So Far

    Related Posts

    News & Updates

    Does Elden Ring Nightreign have crossplay or cross-platform play?

    May 30, 2025
    News & Updates

    Cyberpunk 2077 sequel enters pre-production as Phantom Liberty crosses 10 million copies sold

    May 30, 2025
    Leave A Reply Cancel Reply

    Continue Reading

    New AI Email Marketing Software

    Development

    Blast-RADIUS Vulnerability Affects Widely-Used RADIUS Authentication Protocol

    Development

    DragonFly BSD is a UNIX-like operating system forked from FreeBSD

    Linux

    FunSearch: Making new discoveries in mathematical sciences using Large Language Models

    Artificial Intelligence
    GetResponse

    Highlights

    Development

    Unpatched PHP Voyager Flaws Leave Servers Open to One-Click RCE Exploits

    January 30, 2025

    Three security flaws have been disclosed in the open-source PHP package Voyager that could be…

    tere – terminal file explorer

    December 28, 2024

    UI Interactions & Animations Roundup #44

    June 6, 2024

    CVE-2025-4063 – Code-projects Student Information Management System Buffer Overflow

    April 29, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.