Organizing Design System Component Patterns With CSS Cascade Layers

Organizing Design System Component Patterns With CSS Cascade Layers

I’m making an attempt to provide you with methods to make parts extra customizable, extra environment friendly, and simpler to make use of and perceive, and I need to describe a sample I’ve been leaning into utilizing CSS Cascade Layers.

I get pleasure from organizing code and discover cascade layers a implausible option to set up code explicitly because the cascade seems at it. The neat half is, that as a lot because it helps with “top-level” group, cascade layers may be nested, which permits us to writer extra exact kinds primarily based on the cascade.

The one draw back right here is your creativeness, nothing stops us from over-engineering CSS. And to be clear, chances are you’ll very effectively contemplate what I’m about to point out you as a type of over-engineering. I believe I’ve discovered a steadiness although, maintaining issues easy but organized, and I’d wish to share my findings.

The anatomy of a CSS element sample

Let’s discover a sample for writing parts in CSS utilizing a button for example. Buttons are one of many extra widespread parts present in nearly each element library. There’s good cause for that recognition as a result of buttons can be utilized for a wide range of use instances, together with:

  • performing actions, like opening a drawer,
  • navigating to completely different sections of the UI, and
  • holding some type of state, equivalent to focus or hover.

And buttons are available a number of completely different flavors of markup, like <button>, enter[type="button"], and <a category="button">. There are much more methods to make buttons than that, in the event you can imagine it.

On high of that, completely different buttons carry out completely different features and are sometimes styled accordingly so {that a} button for one sort of motion is distinguished from one other. Buttons additionally reply to state modifications, equivalent to when they’re hovered, energetic, and centered. You probably have ever written CSS with the BEM syntax, we are able to type of assume alongside these traces throughout the context of cascade layers.

.button {}
.button-primary {}
.button-secondary {}
.button-warning {}
/* and so on. */

Okay, now, let’s write some code. Particularly, let’s create a couple of various kinds of buttons. We’ll begin with a .button class that we are able to set on any aspect that we need to be styled as, effectively, a button! We already know that buttons come in numerous flavors of markup, so a generic .button class is probably the most reusable and extensible option to choose one or all of them.

.button {
  /* Kinds frequent to all buttons */
}

Utilizing a cascade layer

That is the place we are able to insert our very first cascade layer! Bear in mind, the rationale we would like a cascade layer within the first place is that it permits us to set the CSS Cascade’s studying order when evaluating our kinds. We are able to inform CSS to guage one layer first, adopted by one other layer, then one other — all in keeping with the order we would like. That is an unimaginable characteristic that grants us superpower management over which kinds “win” when utilized by the browser.

We’ll name this layer parts as a result of, effectively, buttons are a sort of element. What I like about this naming is that it’s generic sufficient to help different parts sooner or later as we determine to develop our design system. It scales with us whereas sustaining a pleasant separation of considerations with different kinds we write down the street that perhaps aren’t particular to parts.

/* Elements top-level layer */
@layer parts {
  .button {
    /* Kinds frequent to all buttons */
  }
}

Nesting cascade layers

Right here is the place issues get somewhat bizarre. Do you know you’ll be able to nest cascade layers inside courses? That’s completely a factor. So, test this out, we are able to introduce a brand new layer contained in the .button class that’s already inside its personal layer. Right here’s what I imply:

/* Elements top-level layer */
@layer parts {

  .button {
    /* Part parts layer */
    @layer parts {
      /* Kinds */
    }
  }
}

That is how the browser interprets that layer inside a layer on the finish of the day:

@layer parts {
  @layer parts {
    .button {
      /* button kinds... */
    }
  }
}

This isn’t a put up simply on nesting kinds, so I’ll simply say that your mileage might fluctuate while you do it. Try Andy Bell’s recent article about using caution with nested styles.

Structuring kinds

To date, we’ve established a .button class inside a cascade layer that’s designed to carry any sort of element in our design system. Inside that .button is one other cascade layer, this one for choosing the various kinds of buttons we would encounter within the markup. We talked earlier about buttons being <button>, <enter>, or <a> and that is how we are able to individually choose model every sort.

We are able to use the :is() pseudo-selector perform as that’s akin to saying, “If this .button is an <a> aspect, then apply these kinds.”

/* Elements top-level layer */
@layer parts {
  .button {
    /* Part parts layer */
    @layer parts {
      /* kinds frequent to all buttons */

      &:is(a) {
        /* <a> particular kinds */
      }

      &:is(button) {
        /* <button> particular kinds */
      }

      /* and so on. */
    }
  }
}

Defining default button kinds

I’m going to fill in our code with the frequent kinds that apply to all buttons. These kinds sit on the high of the parts layer in order that they’re utilized to any and all buttons, whatever the markup. Contemplate them default button kinds, so to talk.

/* Elements top-level layer */
@layer parts {
  .button {
    /* Part parts layer */
    @layer parts {
      background-color: darkslateblue;
      border: 0;
      shade: white;
      cursor: pointer;
      show: grid;
      font-size: 1rem;
      font-family: inherit;
      line-height: 1;
      margin: 0;
      padding-block: 0.65rem;
      padding-inline: 1rem;
      place-content: middle;
      width: fit-content;
    }
  }
}

Defining button state kinds

What ought to our default buttons do when they’re hovered, clicked, or in focus? These are the completely different states that the button would possibly take when the consumer interacts with them, and we have to model these accordingly.

I’m going to create a brand new cascade sub-layer instantly beneath the parts sub-layer known as, creatively, states:

/* Elements top-level layer */
@layer parts {
  .button {
    /* Part parts layer */
    @layer parts {
      /* Kinds frequent to all buttons */
    }

    /* Part states layer */
    @layer states {
      /* Kinds for particular button states */
    }
  }
}

Pause and mirror right here. What states ought to we goal? What can we need to change for every of those states?

Some states might share comparable property modifications, equivalent to :hover and :focus having the identical background shade. Fortunately, CSS offers us the instruments we have to deal with such issues, utilizing the :where() perform to group property modifications primarily based on the state. Why :the place() as an alternative of :is()? :the place() comes with zero specificity, which means it’s loads simpler to override than :is(), which takes the specificity of the aspect with the very best specificity rating in its arguments. Sustaining low specificity is a advantage in the case of writing scalable, maintainable CSS.

/* Part states layer */
@layer states {
  &:the place(:hover, :focus-visible) {
    /* button hover and focus state kinds */
  }
}

However how can we replace the button’s kinds in a significant method? What I imply by that’s how can we make it possible for the button seems prefer it’s hovered or in focus? We may simply slap a brand new background shade on it, however ideally, the colour must be associated to the background-color set within the parts layer.

So, let’s refactor issues a bit. Earlier, I set the .button aspect’s background-color to darkslateblue. I need to reuse that shade, so it behooves us to make that right into a CSS variable so we are able to replace it as soon as and have it apply all over the place. Counting on variables is yet one more advantage of writing scalable and maintainable CSS.

I’ll create a brand new variable known as --button-background-color that’s initially set to darkslateblue after which set it on the default button kinds:

/* Part parts layer */
@layer parts {
  --button-background-color: darkslateblue;

  background-color: var(--button-background-color);
  border: 0;
  shade: white;
  cursor: pointer;
  show: grid;
  font-size: 1rem;
  font-family: inherit;
  line-height: 1;
  margin: 0;
  padding-block: 0.65rem;
  padding-inline: 1rem;
  place-content: middle;
  width: fit-content;
}

Now that now we have a shade saved in a variable, we are able to set that very same variable on the button’s hovered and centered states in our different layer, utilizing the comparatively new color-mix() function to transform darkslateblue to a lighter shade when the button is hovered or in focus.

Again to our states layer! We’ll first combine the colour in a brand new CSS variable known as --state-background-color:

/* Part states layer */
@layer states {
  &:the place(:hover, :focus-visible) {
    /* customized property solely utilized in state */
    --state-background-color: color-mix(
      in srgb, 
      var(--button-background-color), 
      white 10%
    );
  }
}

We are able to then apply that shade because the background shade by updating the background-color property.

/* Part states layer */
@layer states {
  &:the place(:hover, :focus-visible) {
    /* customized property solely utilized in state */
    --state-background-color: color-mix(
      in srgb, 
      var(--button-background-color), 
      white 10%
    );

    /* making use of the state background-color */
    background-color: var(--state-background-color);
  }
}

Defining modified button kinds

Together with parts and states layers, chances are you’ll be in search of some type of variation in your parts, equivalent to modifiers. That’s as a result of not all buttons are going to seem like your default button. You may want one with a inexperienced background shade for the consumer to verify a call. Or maybe you need a pink one to point hazard when clicked. So, we are able to take our current default button kinds and modify them for these particular use instances

If we take into consideration the order of the cascade — all the time flowing from high to backside — we don’t need the modified kinds to have an effect on the kinds within the states layer we simply made. So, let’s add a brand new modifiers layer in between parts and states:

/* Elements top-level layer */
@layer parts {

  .button {
  /* Part parts layer */
  @layer parts {
    /* and so on. */
  }

  /* Part modifiers layer */
  @layer modifiers {
    /* new layer! */
  }

  /* Part states layer */
  @layer states {
    /* and so on. */
  }
}

Just like how we dealt with states, we are able to now replace the --button-background-color variable for every button modifier. We may modify the kinds additional, in fact, however we’re maintaining issues pretty simple to exhibit how this technique works.

We’ll create a brand new class that modifies the background-color of the default button from darkslateblue to darkgreen. Once more, we are able to depend on the :is() selector as a result of we would like the added specificity on this case. That method, we override the default button model with the modifier class. We’ll name this class .success (inexperienced is a “profitable” shade) and feed it to :is():

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

If we add the .success class to one among our buttons, it turns into darkgreen as an alternative darkslateblue which is precisely what we would like. And since we already do some color-mix()-ing within the states layer, we’ll robotically inherit these hover and focus kinds, which means darkgreen is lightened in these states.

/* Elements top-level layer */
@layer parts {
  .button {
    /* Part parts layer */
    @layer parts {
      --button-background-color: darkslateblue;

      background-color: var(--button-background-color);
      /* and so on. */

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

    /* Part states layer */
    @layer states {
      &:the place(:hover, :focus) {
        --state-background-color: color-mix(
          in srgb,
          var(--button-background-color),
          white 10%
        );

        background-color: var(--state-background-color);
      }
    }
  }
}

Placing all of it collectively

We are able to refactor any CSS property we have to modify right into a CSS customized property, which supplies us plenty of room for personalization.

/* Elements top-level layer */
@layer parts {
  .button {
    /* Part parts layer */
    @layer parts {
      --button-background-color: darkslateblue;

      --button-border-width: 1px;
      --button-border-style: stable;
      --button-border-color: clear;
      --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);
      shade: var(--button-text-color);
      cursor: pointer;
      show: 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: middle;
      width: fit-content;
    }

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

      &:is(.ghost) {
        --button-background-color: clear;
        --button-text-color: black;
        --button-border-color: darkslategray;
        --button-border-width: 3px;
      }
    }

    /* Part states layer */
    @layer states {
      &:the place(:hover, :focus) {
        --state-background-color: color-mix(
          in srgb,
          var(--button-background-color),
          white 10%
        );

        background-color: var(--state-background-color);
      }
    }
  }
}

P.S. Look nearer at that demo and take a look at how I’m adjusting the button’s background utilizing light-dark() — then go learn Sara Pleasure’s “Come to the light-dark() Side” for an intensive rundown of how that works!


What do you assume? Is that this one thing you’ll use to arrange your kinds? I can see how making a system of cascade layers might be overkill for a small challenge with few parts. However even somewhat toe-dipping into issues like we simply did illustrates how a lot energy now we have in the case of managing — and even taming — the CSS Cascade. Buttons are deceptively advanced however we noticed how few kinds it takes to deal with the whole lot from the default kinds to writing the kinds for his or her states and modified variations.

Leave a Reply