Tag: Cascade

Taming the Cascade With BEM and Modern CSS Selectors

BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format can be polarizing. But it is – at least in my Twitter bubble – one of the better-liked CSS methodologies.

Personally, I think BEM is good, and I think you should use it. But I also get why you might not.

Regardless of your opinion on BEM, it offers several benefits, the biggest being that it helps avoid specificity clashes in the CSS Cascade. That’s because, if used properly, any selectors written in a BEM format should have the same specificity score (0,1,0). I’ve architected the CSS for plenty of large-scale websites over the years (think government, universities, and banks), and it’s on these larger projects where I’ve found that BEM really shines. Writing CSS is much more fun when you have confidence that the styles you’re writing or editing aren’t affecting some other part of the site.

There are actually exceptions where it is deemed totally acceptable to add specificity. For instance: the :hover and :focus pseudo classes. Those have a specificity score of 0,2,0. Another is pseudo elements — like ::before and ::after — which have a specificity score of 0,1,1. For the rest of this article though, let’s assume we don’t want any other specificity creep. 🤓

But I’m not really here to sell you on BEM. Instead, I want to talk about how we can use it alongside modern CSS selectors — think :is(), :has(), :where(), etc. — to gain even more control of the Cascade.

What’s this about modern CSS selectors?

The CSS Selectors Level 4 spec gives us some powerful new(ish) ways to select elements. Some of my favorites include :is(), :where(), and :not(), each of which is supported by all modern browsers and is safe to use on almost any project nowadays.

:is() and :where() are basically the same thing except for how they impact specificity. Specifically, :where() always has a specificity score of 0,0,0. Yep, even :where(button#widget.some-class) has no specificity. Meanwhile, the specificity of :is() is the element in its argument list with the highest specificity. So, already we have a Cascade-wrangling distinction between two modern selectors that we can work with.

The incredibly powerful :has() relational pseudo-class is also rapidly gaining browser support (and is the biggest new feature of CSS since Grid, in my humble opinion). However, at time of writing, browser support for :has() isn’t quite good enough for use in production just yet.

Lemme stick one of those pseudo-classes in my BEM and…

/* ❌ specificity score: 0,2,0 */ .something:not(.something--special) {   /* styles for all somethings, except for the special somethings */ }

Whoops! See that specificity score? Remember, with BEM we ideally want our selectors to all have a specificity score of 0,1,0. Why is 0,2,0 bad? Consider this same example, expanded:

.something:not(.something--special) {   color: red; } .something--special {   color: blue; }

Even though the second selector is last in the source order, the first selector’s higher specificity (0,2,0) wins, and the color of .something--special elements will be set to red. That is, assuming your BEM is written properly and the selected element has both the .something base class and .something--special modifier class applied to it in the HTML.

Used carelessly, these pseudo-classes can impact the Cascade in unexpected ways. And it’s these sorts of inconsistencies that can create headaches down the line, especially on larger and more complex codebases.

Dang. So now what?

Remember what I was saying about :where() and the fact that its specificity is zero? We can use that to our advantage:

/* ✅ specificity score: 0,1,0 */ .something:where(:not(.something--special)) {   /* etc. */ }

The first part of this selector (.something) gets its usual specificity score of 0,1,0. But :where() — and everything inside it — has a specificity of 0, which does not increase the specificity of the selector any further.

:where() allows us to nest

Folks who don’t care as much as me about specificity (and that’s probably a lot of people, to be fair) have had it pretty good when it comes to nesting. With some carefree keyboard strokes, we may wind up with CSS like this (note that I’m using Sass for brevity):

.card { ... }  .card--featured {   /* etc. */     .card__title { ... }   .card__title { ... } }  .card__title { ... } .card__img { ... }

In this example, we have a .card component. When it’s a “featured” card (using the .card--featured class), the card’s title and image needs to be styled differently. But, as we now know, the code above results in a specificity score that is inconsistent with the rest of our system.

A die-hard specificity nerd might have done this instead:

.card { ... } .card--featured { ... } .card__title { ... } .card__title--featured { ... } .card__img { ... } .card__img--featured { ... }

That’s not so bad, right? Frankly, this is beautiful CSS.

There is a downside in the HTML though. Seasoned BEM authors are probably painfully aware of the clunky template logic that’s required to conditionally apply modifier classes to multiple elements. In this example, the HTML template needs to conditionally add the --featured modifier class to three elements (.card, .card__title, and .card__img) though probably even more in a real-world example. That’s a lot of if statements.

The :where() selector can help us write a lot less template logic — and fewer BEM classes to boot — without adding to the level of specificity.

.card { ... } .card--featured { ... }  .card__title { ... } :where(.card--featured) .card__title { ... }  .card__img { ... } :where(.card--featured) .card__img { ... }

Here’s same thing but in Sass (note the trailing ampersands):

.card { ... } .card--featured { ... } .card__title {    /* etc. */    :where(.card--featured) & { ... } } .card__img {    /* etc. */    :where(.card--featured) & { ... } }

Whether or not you should opt for this approach over applying modifier classes to the various child elements is a matter of personal preference. But at least :where() gives us the choice now!

What about non-BEM HTML?

We don’t live in a perfect world. Sometimes you need to deal with HTML that is outside of your control. For instance, a third-party script that injects HTML that you need to style. That markup often isn’t written with BEM class names. In some cases those styles don’t use classes at all but IDs!

Once again, :where() has our back. This solution is slightly hacky, as we need to reference the class of an element somewhere further up the DOM tree that we know exists.

/* ❌ specificity score: 1,0,0 */ #widget {   /* etc. */ }  /* ✅ specificity score: 0,1,0 */ .page-wrapper :where(#widget) {   /* etc. */ }

Referencing a parent element feels a little risky and restrictive though. What if that parent class changes or isn’t there for some reason? A better (but perhaps equally hacky) solution would be to use :is() instead. Remember, the specificity of :is() is equal to the most specific selector in its selector list.

So, instead of referencing a class we know (or hope!) exists with :where(), as in the above example, we could reference a made up class and the <body> tag.

/* ✅ specificity score: 0,1,0 */ :is(.dummy-class, body) :where(#widget) {   /* etc. */ }

The ever-present body will help us select our #widget element, and the presence of the .dummy-class class inside the same :is() gives the body selector the same specificity score as a class (0,1,0)… and the use of :where() ensures the selector doesn’t get any more specific than that.

That’s it!

That’s how we can leverage the modern specificity-managing features of the :is() and :where() pseudo-classes alongside the specificity collision prevention that we get when writing CSS in a BEM format. And in the not too distant future, once :has() gains Firefox support (it’s currently supported behind a flag at the time of writing) we’ll likely want to pair it with :where() to undo its specificity.

Whether you go all-in on BEM naming or not, I hope we can agree that having consistency in selector specificity is a good thing!

Taming the Cascade With BEM and Modern CSS Selectors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , ,

Using CSS Cascade Layers to Manage Custom Styles in a Tailwind Project

If a utility class only does one thing, chances are you don’t want it to be overridden by any styles coming from elsewhere. One approach is to use !important to be 100% certain the style will be applied, regardless of specificity conflicts.

The Tailwind config file has an !important option that will automatically add !important to every utility class. There’s nothing wrong with using !important this way, but nowadays there are better ways to handle specificity. Using CSS Cascade Layers we can avoid the heavy-handed approach of using !important.

Cascade layers allow us to group styles into “layers”. The precedence of a layer always beats the specificity of a selector. Specificity only matters inside each layer. Establishing a sensible layer order helps avoid styling conflicts and specificity wars. That’s what makes CSS Cascade Layers a great tool for managing custom styles alongside styles from third-party frameworks, like Tailwind.

A Tailwind source .css file usually starts something like this:

@tailwind base; @tailwind components; @tailwind utilities; @tailwind variants;

Let’s take a look at the official Tailwind docs about directives:

Directives are custom Tailwind-specific at-rules you can use in your CSS that offer special functionality for Tailwind CSS projects. Use the @tailwind directive to insert Tailwind’s base, components, utilities and variants styles into your CSS.

In the output CSS file that gets built, Tailwind’s CSS reset — known as Preflight — is included first as part of the base styles. The rest of base consists of CSS variables needed for Tailwind to work. components is a place for you to add your own custom classes. Any utility classes you’ve used in your markup will appear next. Variants are styles for things like hover and focus states and responsive styles, which will appear last in the generated CSS file.

The Tailwind @layer directive

Confusingly, Tailwind has its own @layer syntax. This article is about the CSS standard, but let’s take a quick look at the Tailwind version (which gets compiled away and doesn’t end up in the output CSS). The Tailwind @layer directive is a way to inject your own extra styles into a specified part of the output CSS file.

For example, to append your own styles to the base styles, you would do the following:

@layer base {   h1 {     font-size: 30px;   } }

The components layer is empty by default — it’s just a place to put your own classes. If you were doing things the Tailwind way, you’d probably use @apply (although the creator of Tailwind recently advised against it), but you can also write classes the regular way:

@layer components {   .btn-blue {     background-color: blue;     color: white;   } }

The CSS standard is much more powerful. Let’s get back to that…

Using the CSS standard @layer

Here’s how we can rewrite this to use the CSS standard @layer:

@layer tailwind-base, my-custom-styles, tailwind-utilities;  @layer tailwind-base {   @tailwind base; }  @layer tailwind-utilities {   @tailwind utilities;   @tailwind variants; } 

Unlike the Tailwind directive, these don’t get compiled away. They’re understood by the browser. In fact, DevTools in Edge, Chrome, Safari, and Firefox will even show you any layers you’ve defined.

CSS Cascade Layers with Tailwind CSS layers in DevTools.

You can have as many layers as you want — and name them whatever you want — but in this example, all my custom styles are in a single layer (my-custom-styles). The first line establishes the layer order:

@layer tailwind-base, my-custom-styles, tailwind-utilities;

This needs to be provided upfront. Be sure to include this line before any other code that uses @layer. The first layer in the list will be the least powerful, and the last layer in the list will be the most powerful. That means tailwind-base is the least powerful layer and any code in it will be overridden by all the subsequent layers. That also means tailwind-utilities will always trump any other styles — regardless of source order or specificity. (Utilities and variants could go in separate layers, but the maintainers of Tailwind will ensure variants always trump utilities, so long as you include the variants below the utilities directive.)

Anything that isn’t in a layer will override anything that is in a layer (with the one exception being styles that use !important). So, you could also opt to leave utilities and variants outside of any layer:

@layer tailwind-base, tailwind-components, my-custom-styles;  @layer tailwind-base {   @tailwind base; }  @layer tailwind-components {   @tailwind components; }  @tailwind utilities; @tailwind variants;

What did this actually buy us? There are plenty of times when advanced CSS selectors come in pretty handy. Let’s create a version of :focus-within that only responds to keyboard focus rather than mouse clicks using the :has selector (which lands in Chrome 105). This will style a parent element when any of its children receive focus. Tailwind 3.1 introduced custom variants — e.g. <div class="[&:has(:focus-visible)]:outline-red-600"> — but sometimes it’s easier to just write CSS:

@layer tailwind-base, my-custom-styles; @layer tailwind-base {   @tailwind base; }  @tailwind utilities;  @layer my-custom-styles {   .radio-container {     padding: 4px 24px;     border: solid 2px rgb(230, 230, 230);   }   .radio-container:has(:focus-visible) {     outline: solid 2px blue;   } }

Let’s say in just one instance we want to override the outline-color from blue to something else. Let’s say the element we’re working with has both the Tailwind class .outline-red-600 and our own .radio-container:has(:focus-visible) class:

<div class="outline-red-600 radio-container"> ... </div>

Which outline-color will win?

Ordinarily, the higher specificity of .radio-container:has(:focus-visible) would mean the Tailwind class has no effect — even if it’s lower in the source order. But, unlike the Tailwind @layer directive that relies on source order, the CSS standard @layer overrules specificity.

As a result, we can use complex selectors in our own custom styles but still override them with Tailwind’s utility classes when we need to — without having to resort to heavy-handed !important usage to get what we want.

Using CSS Cascade Layers to Manage Custom Styles in a Tailwind Project originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , , , , , ,

A Complete Guide to CSS Cascade Layers

This is your complete guide to CSS cascade layers, a CSS feature that allows us to define explicit contained layers of specificity, so that we have full control over which styles take priority in a project without relying on specificity hacks or !important. This guide is intended to help you fully understand what cascade layers are for, how and why you might choose to use them, the current levels of support, and the syntax of how you use them.

Table of Contents

Quick example

/* establish a layer order up-front, from lowest to highest priority */ @layer reset, defaults, patterns, components, utilities, overrides;  /* import stylesheets into a layer (dot syntax represents nesting) */ @import url('framework.css') layer(components.framework);  /* add styles to layers */ @layer utilities {   /* high layer priority, despite low specificity */   [data-color='brand'] {      color: var(--brand, rebeccapurple);   } }  @layer defaults {   /* higher specificity, but lower layer priority */   a:any-link { color: maroon; } }  /* un-layered styles have the highest priority */ a {   color: mediumvioletred; }

Introduction: what are cascade layers?

CSS Cascade Layers are intended to solve tricky problems in CSS. Let’s take a look at the main problem and how cascade layers aim to solve it.

Problem: Specificity conflicts escalate

Many of us have been in situations where we want to override styles from elsewhere in our code (or a third-party tool), due to conflicting selectors. And over the years, authors have developed a number of “methodologies” and “best practices” to avoid these situations — such as “only using a single class” for all selectors. These rules are usually more about avoiding the cascade, rather than putting it to use.

Managing cascade conflicts and selector specificity has often been considered one of the harder — or at least more confusing — aspects of CSS. That may be partly because few other languages rely on a cascade as their central feature, but it’s also true that the original cascade relies heavily on heuristics (an educated-guess or assumption built into the code) rather than providing direct and explicit control to web authors.

Selector specificity, for example — our primary interaction with the cascade — is based on the assumption that more narrowly targeted styles (like IDs that are only used once) are likely more important than more generic and reusable styles (like classes and attributes). That is to say: how specific the selector is. That’s a good guess, but it’s not a totally reliable rule, and that causes some issues:

  • It combines the act of selecting elements, with the act of prioritizing rule-sets.
  • The simplest way to ‘fix’ a conflict with specificity is to escalate the problem by adding otherwise unnecessary selectors, or (gasp) throwing the !important hand-grenade.
.overly#powerful .framework.widget {   color: maroon; }  .my-single_class { /* add some IDs to this ??? */   color: rebeccapurple; /* add !important ??? */ }

Solution: cascade layers provide control

Cascade layers give CSS authors more direct control over the cascade so we can build more intentionally cascading systems without relying as much on heuristic assumptions that are tied to selection.

Using the @layer at-rule and layered @imports, we can establish our own layers of the cascade — building from low-priority styles like resets and defaults, through themes, frameworks, and design systems, up to highest-priority styles, like components, utilities, and overrides. Specificity is still applied to conflicts within each layer, but conflicts between layers are always resolved by using the higher-priority layer styles.

@layer framework {   .overly#powerful .framework.widget {     color: maroon;   } }  @layer site {   .my-single_class {     color: rebeccapurple;   } }

These layers are ordered and grouped so that they don’t escalate in the same way that specificity and importance can. Cascade layers aren’t cumulative like selectors. Adding more layers doesn’t make something more important. They’re also not binary like importance — suddenly jumping to the top of a stack — or numbered like z-index, where we have to guess a big number (z-index: 9999999?). In fact, by default, layered styles are less important than un-layered styles.

@layer defaults {   a:any-link { color: maroon; } }  /* un-layered styles have the highest priority */ a {   color: mediumvioletred; }

Where do layers fit in the cascade?

The cascade is a series of steps (an algorithm) for resolving conflicts between styles.

html { --button: teal; } button { background: rebeccapurple !important; } .warning { background: maroon; }

<button class="warning" style="background: var(--button);">   what color background? </button>

With the addition of cascade layers, those steps are:

Illustration of the various specificity levels of the CSS cascade and where CSS Cascade Layers fit in it.

Selector specificity is only one small part of the cascade, but it’s also the step we interact with most, and is often used to refer more generally to overall cascade priority. People might say that the !important flag or the style attribute “adds specificity” — a quick way of expressing that the style becomes higher priority in the cascade. Since cascade layers have been added directly above specificity, it’s reasonable to think about them in a similar way: one step more powerful than ID selectors.

However, CSS Cascade Layers also make it more essential that we fully understand the role of !important in the cascade — not just as a tool for “increasing specificity” but as a system for balancing concerns.

!important origins, context, and layers are reversed!

As web authors, we often think of !important as a way of increasing specificity, to override inline styles or highly specific selectors. That works OK in most cases (if you’re OK with the escalation) but it leaves out the primary purpose of importance as a feature in the overall cascade.

Importance isn’t there to simply increase power — but to balance the power between various competing concerns.

Important origins

It all starts with origins, where a style comes from in the web ecosystem. There are three basic origins in CSS:

  • The browser (or user agent)
  • The user (often via browser preferences)
  • Web authors (that’s us!)

Browsers provide readable defaults for all the elements, and then users set their preferences, and then we (authors) provide the intended design for our web pages. So, by default, browsers have the lowest priority, user preferences override the browser defaults, and we’re able to override everyone.

But the creators of CSS were very clear that we should not actually have the final word:

If conflicts arise the user should have the last word, but one should also allow the author to attach style hints.

— Håkon Lie (emphasis added)

So importance provides a way for the browser and users to re-claim their priority when it matters most. When the !important flag is added to a style, three new layers are created — and the order is reversed!

  1. !important browser styles (most powerful)
  2. !important user preferences
  3. !important author styles
  4. normal author styles
  5. normal user preferences
  6. normal browser styles (least powerful)

For us, adding !important doesn’t change much — our important styles are pretty close to our normal styles — but for the browser and user it’s a very powerful tool for regaining control. Browser default style sheets include a number of important styles that it would be impossible for us to override, such as:

iframe:fullscreen {   /* iframes in full-screen mode don't show a border. */   border: none !important;   padding: unset !important; }

While most of the popular browsers have made it difficult to upload actual user stylesheets, they all offer user preferences: a graphic interface for establishing specific user styles. In that interface, there is always a checkbox available for users to choose if a site is allowed to override their preferences or not. This is the same as setting !important in a user stylesheet:

Screenshot of user font preferences.

Important context

The same basic logic is applied to context in the cascade. By default, styles from the host document (light DOM) override styles from an embedded context (shadow DOM). However, adding !important reverses the order:

  1. !important shadow context (most powerful)
  2. !important host context
  3. normal host context
  4. normal shadow context (least powerful)

Important styles that come from inside a shadow context override important styles defined by the host document. Here’s an odd-bird custom element with some styles written in the element template (shadow DOM), and some styles in the host page (light DOM) stylesheet:

Both color declarations have normal importance, and so the host page mediumvioletred takes priority. But the font-family declarations are flagged !important, giving advantage to the shadow-context, where fantasy is defined.

Important layers

Cascade layers work the same way as both origins and context, with the important layers in reverse-order. The only difference is that layers make that behavior much more noticeable.

Once we start using cascade layers, we will need to be much more cautious and intentional about how we use !important. It’s no longer a quick way to jump to the top of the priorities — but an integrated part of our cascade layering; a way for lower layers to insist that some of their styles are essential.

Since cascade layers are customizable, there’s no pre-defined order. But we can imagine starting with three layers:

  1. utilities (most powerful)
  2. components
  3. defaults (least powerful)

When styles in those layers are marked as important, they would generate three new, reversed important layers:

  1. !important defaults (most powerful)
  2. !important components
  3. !important utilities
  4. normal utilities
  5. normal components
  6. normal defaults (least powerful)

In this example, the color is defined by all three normal layers, and the utilities layer wins the conflict, applying the maroon color, as the utilities layer has a higher priority in @layers. But notice that the text-decoration property is marked !important in both the defaults and components layers, where important defaults take priority, applying the underline declared by defaults:

Establishing a layer order

We can create any number of layers and name them or group them in various ways. But the most important thing to do is to make sure our layers are applied in the right order of priority.

A single layer can be used multiple times throughout the codebase — cascade layers stack in the order they first appear. The first layer encountered sits at the bottom (least powerful), and the last layer at the top (most powerful). But then, above that, un-layered styles have the highest priority:

@layer layer-1 { a { color: red; } } @layer layer-2 { a { color: orange; } } @layer layer-3 { a { color: yellow; } } /* un-layered */ a { color: green; }
  1. un-layered styles (most powerful)
  2. layer-3
  3. layer-2
  4. layer-1 (least powerful)

Then, as discussed above, any important styles are applied in a reverse order:

@layer layer-1 { a { color: red !important; } } @layer layer-2 { a { color: orange !important; } } @layer layer-3 { a { color: yellow !important; } } /* un-layered */ a { color: green !important; }
  1. !important layer-1 (most powerful)
  2. !important layer-2
  3. !important layer-3
  4. !important un-layered styles
  5. normal un-layered styles
  6. normal layer-3
  7. normal layer-2
  8. normal layer-1 (least powerful)

Layers can also be grouped, allowing us to do more complicated sorting of top-level and nested layers:

@layer layer-1 { a { color: red; } } @layer layer-2 { a { color: orange; } } @layer layer-3 {   @layer sub-layer-1 { a { color: yellow; } }   @layer sub-layer-2 { a { color: green; } }   /* un-nested */ a { color: blue; } } /* un-layered */ a { color: indigo; }
  1. un-layered styles (most powerful)
  2. layer-3
    1. layer-3 un-nested
    2. layer-3 sub-layer-2
    3. layer-3 sub-layer-1
  3. layer-2
  4. layer-1 (least powerful)

Grouped layers always stay together in the final layer order (for example, sub-layers of layer-3 will all be next to each other), but this otherwise behaves the same as if the list was “flattened” — turning this into a single six-item list. When reversing !important layer order, the entire list flattened is reversed.

But layers don’t have to be defined once in a single location. We give them names so that layers can be defined in one place (to establish layer order), and then we can append styles to them from anywhere:

/* describe the layer in one place */ @layer my-layer;  /* append styles to it from anywhere */ @layer my-layer { a { color: red; } }

We can even define a whole ordered list of layers in a single declaration:

@layer one, two, three, four, five, etc;

This makes it possible for the author of a site to have final say over the layer order. By providing a layer order up-front, before any third party code is imported, the order can be established and rearranged in one place without worrying about how layers are used in any third-party tool.

Syntax: Working with cascade layers

Let’s take a look at the syntax!

Order-setting @layer statements

Since layers are stacked in the order they are defined, it’s important that we have a tool for establishing that order all in one place!

We can use @layer statements to do that. The syntax is:

@layer <layer-name>#;

That hash (#) means we can add as many layer names as we want in a comma-separated list:

@layer reset, defaults, framework, components, utilities;

That will establish the layer order:

  1. un-layered styles (most powerful)
  2. utilities
  3. components
  4. framework
  5. defaults
  6. reset (least powerful)

We can do this as many times as we want, but remember: what matters is the order each name first appears. So this will have the same result:

@layer reset, defaults, framework; @layer components, defaults, framework, reset, utilities;

The ordering logic will ignore the order of reset, defaults, and framework in the second @layer rule because those layers have already been established. This @layer list syntax doesn’t add any special magic to the layer ordering logic: layers are stacked based on the order in which the layers first appear in your code. In this case, reset appears first in the first @layer list. Any @layer statement that comes later can only append layer names to the list, but can’t move layers that already exist. This ensures that you can always control the final overall layer order from one location — at the very start of your styles.

These layer-ordering statements are allowed at the top of a stylesheet, before the @import rule (but not between imports). We highly recommend using this feature to establish all your layers up-front in a single place so you always know where to look or make changes.

Block @layer rules

The block version of the @layer rule only takes a single layer name, but then allows you to add styles to that layer:

@layer <layer-name> {   /* styles added to the layer */ }

You can put most things inside an @layer block — media queries, selectors and styles, support queries, etc. The only things you can’t put inside a layer block are things like charset, imports, and namespaces. But don’t worry, there is a syntax for importing styles into a layer.

If the layer name hasn’t been established before, this layer rule will add it to the layer order. But if the name has been established, this allows you to add styles to existing layers from anywhere in the document — without changing the priority of each layer.

If we’ve established our layer-order up-front with the layer statement rule, we no longer need to worry about the order of these layer blocks:

/* establish the order up-front */ @layer defaults, components, utilities;  /* add styles to layers in any order */ @layer utilities {   [hidden] { display: none; } }  /* utilities will override defaults, based on established order */ @layer defaults {   * { box-sizing: border-box; }   img { display: block; } }

Grouping (nested) layers

Layers can be grouped, by nesting layer rules:

@layer one {   /* sorting the sub-layers */   @layer two, three;    /* styles ... */   @layer three { /* styles ... */ }   @layer two { /* styles ... */ } }

This generates grouped layers that can be represented by joining the parent and child names with a period. That means the resulting sub-layers can also be accessed directly from outside the group:

/* sorting nested layers directly */ @layer one.two, one.three;  /* adding to nested layers directly */ @layer one.three { /* ... */ } @layer one.two { /* ... */ }

The rules of layer-ordering apply at each level of nesting. Any styles that are not further nested are considered “un-layered” in that context, and have priority over further nested styles:

@layer defaults {   /* un-layered defaults (higher priority) */   :any-link { color: rebeccapurple; }    /* layered defaults (lower priority) */   @layer reset {     a[href] { color: blue; }   } }

Grouped layers are also contained within their parent, so that the layer order does not intermix across groups. In this example, the top level layers are sorted first, and then the layers are sorted within each group:

@layer reset.type, default.type, reset.media, default.media;

Resulting in a layer order of:

  • un-layered (most powerful)
  • default group
    • default un-layered
    • default.media
  • default.type
  • reset group
    • reset un-layered
    • reset.media
    • reset.type

Note that layer names are also scoped so that they don’t interact or conflict with similarly-named layers outside their nested context. Both groups can have distinct media sub-layers.

This grouping becomes especially important when using @import or <link> to layer entire stylesheets. A third-party tool, like Bootstrap, could use layers internally — but we can nest those layers into a shared bootstrap layer-group on import, to avoid potential layer-naming conflicts.

Layering entire stylesheets with @import or <link>

Entire stylesheets can be added to a layer using the new layer() function syntax with @import rules:

/* styles imported into to the <layer-name> layer */ @import url('example.css') layer(<layer-name>);

There is also a proposal to add a layer attribute in the HTML <link> element — although this is still under development, and not yet supported anywhere. This can be used to import third-party tools or component libraries, while grouping any internal layers together under a single layer name — or as a way of organizing layers into distinct files.

Anonymous (un-named) layers

Layer names are helpful as they allow us to access the same layer from multiple places for sorting or combining layer blocks — but they are not required.

It’s possible to create anonymous (un-named) layers using the block layer rule:

@layer { /* ... */ } @layer { /* ... */ }

Or using the import syntax, with a layer keyword in place of the layer() function:

/* styles imported into to a new anonymous layer */ @import url('../example.css') layer;

Each anonymous layer is unique, and added to the layer order where it is encountered. Anonymous layers can’t be referenced from other layer rules for sorting or appending more styles.

These should probably be used sparingly, but there might be a few use cases:

  • Projects could ensure that all styles for a given layer are required to be located in a single place.
  • Third-party tools could “hide” their internal layering inside anonymous layers so that they don’t become part of the tool’s public API.

Reverting values to the previous layer

There are several ways that we can use to “revert” a style in the cascade to a previous value, defined by a lower priority origin or layer. That includes a number of existing global CSS values, and a new revert-layer keyword that will also be global (works on any property).

Context: Existing global cascade keywords*

CSS has several global keywords which can be used on any property to help roll-back the cascade in various ways.

  • initial sets a property to the specified value before any styles (including browser defaults) are applied. This can be surprising as we often think of browser styles as the initial value — but, for example, the initial value of display is inline, no matter what element we use it on.
  • inherit sets the property to apply a value from its parent element. This is the default for inherited properties, but can still be used to remove a previous value.
  • unset acts as though simply removing all previous values — so that inherited properties once again inherit, while non-inherited properties return to their initial value.
  • revert only removes values that we’ve applied in the author origin (i.e. the site styles). This is what we want in most cases, since it allows the browser and user styles to remain intact.
New: the revert-layer keyword

Cascade layers add a new global revert-layer keyword. It works the same as revert, but only removes values that we’ve applied in the current cascade layer. We can use that to roll back the cascade, and use whatever value was defined in the previous layers.

In this example, the no-theme class removes any values set in the theme layer.

@layer default {   a { color: maroon; } }  @layer theme {   a { color: var(--brand-primary, purple); }    .no-theme {     color: revert-layer;   } }

So a link tag with the .no-theme class will roll back to use the value set in the default layer. When revert-layer is used in un-layered styles, it behaves the same as revert — rolling back to the previous origin.

Reverting important layers

Things get interesting if we add !important to the revert-layer keyword. Because each layer has two distinct “normal” and “important” positions in the cascade, this doesn’t simply change the priority of the declaration — it changes what layers are reverted.

Let’s assume we have three layers defined, in a layer stack that looks like this:

  1. utilities (most powerful)
  2. components
  3. defaults (least powerful)

We can flesh that out to include not just normal and important positions of each layer, but also un-layered styles, and animations:

  1. !important defaults (most powerful)
  2. !important components
  3. !important utilities
  4. !important un-layered styles
  5. CSS animations
  6. normal un-layered styles
  7. normal utilities
  8. normal components
  9. normal defaults (least powerful)

Now, when we use revert-layer in a normal layer (let’s use utilities) the result is fairly direct. We revert only that layer, while everything else applies normally:

  1. !important defaults (most powerful)
  2. !important components
  3. !important utilities
  4. !important un-layered styles
  5. ✅ CSS animations
  6. ✅ normal un-layered styles
  7. ❌ normal utilities
  8. ✅ normal components
  9. ✅ normal defaults (least powerful)

But when we move that revert-layer into the important position, we revert both the normal and important versions along with everything in-between:

  1. !important defaults (most powerful)
  2. !important components
  3. !important utilities
  4. !important un-layered styles
  5. ❌ CSS animations
  6. ❌ normal un-layered styles
  7. ❌ normal utilities
  8. ✅ normal components
  9. ✅ normal defaults (least powerful)

Use cases: When would I want to use cascade layers?

So what sort of situations might we find ourselves using cascade layers? Here are several examples of when cascade layers make a lot of sense, as well as others where they do not make a lot sense.

Less intrusive resets and defaults

One of the clearest initial use cases would be to make low-priority defaults that are easy to override.

Some resets have been doing this already by applying the :when() pseudo-class around each selector. :when() removes all specificity from the selectors it is applied to, which has the basic impact desired, but also some downsides:

  • It has to be applied to each selector individually
  • Conflicts inside the reset have to be resolved without specificity

Layers allow us to more simply wrap the entire reset stylesheet, either using the block @layer rule:

/* reset.css */ @layer reset {   /* all reset styles in here */ }

Or when you import the reset:

/* reset.css */ @import url(reset.css) layer(reset);

Or both! Layers can be nested without changing their priority. This way, you can use a third-party reset, and ensure it gets added to the layer you want whether or not the reset stylesheet itself is written using layers internally.

Since layered styles have a lower priority than default “un-layered” styles, this is a good way to start using cascade layers without re-writing your entire CSS codebase.

The reset selectors still have specificity information to help resolve internal conflicts, without wrapping each individual selector — but you also get the desired outcome of a reset stylesheet that is easy to override.

Managing a complex CSS architecture

As projects become larger and more complex, it can be useful to define clearer boundaries for naming and organizing CSS code. But the more CSS we have, the more potential we have for conflicts — especially from different parts of a system like a “theme” or a “component library” or a set of “utility classes.”

Not only do we want these organized by function, but it can also be useful to organize them based on what parts of the system take priority in the case of a conflict. Harry Robert’s Inverted Triangle CSS does a good job visualizing what those layers might contain.

In fact, the initial pitch for adding layers to the CSS cascade used the ITCSS methodology as a primary example, and a guide for developing the feature.

There is no particular technique required for this, but it’s likely helpful to restrict projects to a pre-defined set of top-level layers and then extend that set with nested layers as appropriate.

For example:

  1. low level reset and normalization styles
  2. element defaults, for basic typography and legibility
  3. themes, like light and dark modes
  4. re-usable patterns that might appear across multiple components
  5. layouts and larger page structures
  6. individual components
  7. overrides and utilities

We can create that top-level layer stack at the very start of our CSS, with a single layer statement:

@layer   reset,   default,   themes,   patterns,   layouts,   components,   utilities;

The exact layers needed, and how you name those layers, might change from one project to the next.

From there, we create even more detailed layer breakdowns. Maybe our components themselves have defaults, structures, themes, and utilities internally.

@layer components {   @layer defaults, structures, themes, utilities; }

Without changing the top-level structure, we now have a way to further layer the styles within each component.

Using third-party tools and frameworks

Integrating third-party CSS with a project is one of the most common places to run into cascade issues. Whether we’re using a shared reset like Normalizer or CSS Remedy, a generic design system like Material Design, a framework like Bootstrap, or a utility toolkit like Tailwind — we can’t always control the selector specificity or importance of all the CSS being used on our sites. Sometimes, this even extends to internal libraries, design systems, and tools managed elsewhere in an organization.

As a result, we often have to structure our internal CSS around the third-party code, or escalate conflicts when they come up — with artificially high specificity or !important flags. And then we have to maintain those hacks over time, adapting to upstream changes.

Cascade layers give us a way to slot third-party code into the cascade of any project exactly where we want it to live — no matter how selectors are written internally. Depending on the type of library we’re using, we might do that in various ways. Let’s start with a basic layer-stack, working our way up from resets to utilities:

@layer reset, type, theme, components, utilities;

And then we can incorporate some tools…

Using a reset

If we’re using a tool like CSS Remedy, we might also have some reset styles of our own that we want to include. Let’s import CSS Remedy into a sub-layer of reset:

@import url('remedy.css') layer(reset.remedy);

Now we can add our own reset styles to the reset layer, without any further nesting (unless we want it). Since styles directly in reset will override any further nested styles, we can be sure our styles will always take priority over CSS Remedy if there’s a conflict — no matter what changes in a new release:

@import url('remedy.css') layer(reset.remedy);  @layer reset {   :is(ol, ul)[role='list'] {     list-style: none;     padding-inline-start: 0;   } }

And since the reset layer is at the bottom of the stack, the rest of the CSS in our system will override both Remedy, and our own local reset additions.

Using utility classes

At the other end of our stack, “utility classes” in CSS can be a useful way to reproduce common patterns (like additional context for screen readers) in a broadly-applicable way. Utilities tend to break the specificity heuristic, since we want them defined broadly (resulting in a low specificity), but we also generally want them to “win” conflicts.

By having a utilities layer at the top of our layer stack, we can make that possible. We can use that in a similar way to the reset example, both loading external utilities into a sub-layer, and providing our own:

@import url('tailwind.css') layer(utilities.tailwind);  @layer utilities {   /* from https://kittygiraudel.com/snippets/sr-only-class/ */   /* but with !important removed from the properties */   .sr-only {     border: 0;     clip: rect(1px, 1px, 1px, 1px);     -webkit-clip-path: inset(50%);     clip-path: inset(50%);     height: 1px;     overflow: hidden;     margin: -1px;     padding: 0;     position: absolute;     width: 1px;     white-space: nowrap;   } }
Using design systems and component libraries

There are a lot of CSS tools that fall somewhere in the middle of our layer stack — combining typography defaults, themes, components, and other aspects of a system.

Depending on the particular tool, we might do something similar to the reset and utility examples above — but there are a few other options. A highly integrated tool might deserve a top-level layer:

@layer reset, bootstrap, utilities; @import url('bootstrap.css') layer(bootstrap);

If these tools start to provide layers as part of their public API, we could also break it down into parts — allowing us to intersperse our code with the library:

@import url('bootstrap/reset.css') layer(reset.bootstrap); @import url('bootstrap/theme.css') layer(theme.bootstrap); @import url('bootstrap/components.css') layer(components.bootstrap);  @layer theme.local {   /* styles here will override theme.bootstrap */   /* but not interfere with styles from components.bootstrap */ }

Using layers with existing (un-layered, !important-filled) frameworks

As with any major language change, there’s going to be an adjustment period when CSS Cascade Layers become widely adopted. What happens if your team is ready to start using layers next month, but your favorite framework decides to wait another three years before they switch over to layered styles? Many frameworks will likely still use !important more often than we’d like! With !important layers reversed, that’s not ideal.

Still, layers can still help us solve the problem. We just have to get clever about it. We decide what layers we want for our project, and that means we can add layers above and also below the framework layers we create.

For now, though, we can use a lower layer to override !important styles from the framework, and a higher layer to override normal styles. Something like this:

@layer framework.important, framework.bootstrap, framework.local; @import url('bootstrap.css') layer(framework.bootstrap);  @layer framework.local {   /* most of our normal framework overrides can live here */ }  @layer framework.important {   /* add !important styles in a lower layer */   /* to override any !important framework styles */ }

It still feels like a bit of a hack, but it helps move us in the right direction — towards a more structured cascade. Hopefully it’s a temporary fix.

Designing a CSS tool or framework

For anyone maintaining a CSS library, cascade layers can help with internal organization, and even become part of the developer API. By naming internal layers of a library, we can allow users of our framework to hook into those layers when customizing or overriding our provided styles.

For example, Bootstrap could expose layers for their “reboot,” “grid,” and “utilities” — likely stacked in that order. Now a user can decide if they want to load those Bootstrap layers into different local layers:

@import url(bootstrap/reboot.css) layer(reset); /* reboot » reset.reboot */ @import url(bootstrap/grid.css) layer(layout); /* grid » layout.grid */ @import url(bootstrap/utils.css) layer(override); /* utils » override.utils */

Or the user might load them into a Bootstrap layer, with local layers interspersed:

@layer bs.reboot, bs.grid, bs.grid-overrides, bs.utils, bs.util-overrides; @import url('bootstrap-all.css') layer(bs);

It’s also possible to hide internal layering from users, when desired, by grouping any private/internal layers inside an anonymous (un-named) layer. Anonymous layers will get added to the layer order where they are encountered, but will not be exposed to users re-arranging or appending styles.

I just want this one property to be more !important

Counter to some expectations, layers don’t make it easy to quickly escalate a particular style so that it overrides another.

If the majority of our styles are un-layered, then any new layer will be de-prioritized in relation to the default. We could do that to individual style blocks, but it would quickly become difficult to track.

Layers are intended to be more foundational, not style-by-style, but establishing consistent patterns across a project. Ideally, if we’ve set that up right, we get the correct result by moving our style to the appropriate (and pre-defined) layer.

If the majority of our styles already fall into well-defined layers, we can always consider adding a new highest-power layer at the top of a given stack, or using un-layered styles to override the layers. We might even consider having a debug layer at the top of the stack, for doing exploratory work outside of production.

But adding new layers on-the-fly can defeat the organizational utility of this feature, and should be used carefully. It’s best to ask: Why should this style override the other?

If the answer has to do with one type of style always overriding another type, layers are probably the right solution. That might be because we’re overriding styles that come from a place we don’t control, or because we’re writing a utility, and it should move into our utilities layer. If the answer has to do with more targeted styles overriding less targeted styles, we might consider making the selectors reflect that specificity.

Or, on rare occasions, we might even have styles that really are important — the feature simply doesn’t work if you override this particular style. We might say adding display: none to the [hidden] attribute belongs in our lowest-priority reset, but should still be hard to override. In that case, !important really is the right tool for the job:

@layer reset {   [hidden] { display: none !important; } }

Scoping and name-spacing styles? Nope!

Cascade layers are clearly an organizational tool, and one that ‘captures’ the impact of selectors, especially when they conflict. So it can be tempting at first glance to see them as a solution for managing scope or name-spacing.

A common first-instinct is to create a layer for each component in a project — hoping that will ensure (for example) that .post-title is only applied inside a .post.

But cascade conflicts are not the same as naming conflicts, and layers aren’t particularly well designed for this type of scoped organization. Cascade layers don’t constrain how selectors match or apply to the HTML, only how they cascade together. So unless we can be sure that component X always override component Y, individual component layers won’t help much. Instead, we’ll need to keep an eye on the proposed @scope spec that is being developed.

It can be useful to think of layers and component-scopes instead as overlapping concerns:

An illustration showing how CSS Cascade Layers can be organized by scope, such as buttons, cards, and login layers that fall into component, theme, and default scopes.

Scopes describe what we are styling, while layers describe why we are styling. We can also think of layers as representing where the style comes from, while scopes represent what the style will attach to.

Test your knowledge: Which style wins?

For each situation, assume this paragraph:

<p id="intro">Hello, World!</p>

Question 1

@layer ultra-high-priority {   #intro {     color: red;   } }  p {   color: green; }
What color is the paragraph?

Despite the layer having a name that sounds pretty important, un-layered styles have a higher priority in the cascade. So the paragraph will be green.

Question 2

@layer ren, stimpy;  @layer ren {   p { color: red !important; } }  p { color: green; }  @layer stimpy {   p { color: blue !important; } }
What color is the paragraph?

Our normal layer order is established at the start — ren at the bottom, then stimpy, then (as always) un-layered styles at the top. But these styles aren’t all normal, some of them are important. Right away, we can filter down to just the !important styles, and ignore the unimportant green. Remember that ‘origins and importance’ are the first step of the cascade, before we even take layering into account.

That leaves us with two important styles, both in layers. Since our important layers are reversed, ren moves to the top, and stimpy to the bottom. The paragraph will be red.

Question 3

@layer Montagues, Capulets, Verona;  @layer Montagues.Romeo { #intro { color: red; } } @layer Montagues.Benvolio { p { color: orange; } }  @layer Capulets.Juliet { p { color: yellow; } } @layer Verona { * { color: blue; } } @layer Capulets.Tybalt { #intro { color: green; } }
What color is the paragraph?

All our styles are in the same origin and context, none are marked as important, and none of them are inline styles. We do have a broad range of selectors here, from a highly specific ID #intro to a zero specificity universal * selector. But layers are resolved before we take specificity into account, so we can ignore the selectors for now.

The primary layer order is established up front, and then sub-layers are added internally. But sub-layers are sorted along with their parent layer — meaning all the Montagues will have lowest priority, followed by all the Capulets, and then Verona has final say in the layer order. So we can immediately filter down to just the Verona styles, which take precedence. Even though the * selector has zero specificity, it will win.

Be careful about putting universal selectors in powerful layers!

Debugging layer conflicts in browser developer tools

Chrome, Safari, Firefox, and Edge browsers all have developer tools that allow you to inspect the styles being applied to a given element on the page. The styles panel of this element inspector will show applied selectors, sorted by their cascade priority (highest priority at the top), and then inherited styles below. Styles that are not being applied for any reason will generally be grayed out, or even crossed out — sometimes with additional information about why the style is not applied. This is the first place to look when debugging any aspect of the cascade, including layer conflicts.

Safari Technology Preview and Firefox Nightly already show (and sort) cascade layers in this panel. This tooling is expected to role out in the stable versions at the same time as cascade layers. The layer of each selector is listed directly above the selector itself:

Showing CSS Cascade Layers in Safari DevTools.

Showing CSS Cascade Layers in FireFox DevTools.

Chrome/Edge are working on similar tools and expect to have them available in Canary (nightly) releases by the time cascade layers land in the stable release. We’ll make updates here as those tools change and improve.

Browser support and fallbacks

Cascade layers are (or will soon be) available by default in all the three major browser engines:

  • Chrome/Edge 99+
  • Firefox 97+
  • Safari (currently in the Technology Preview)

Since layers are intended as foundational building blocks of an entire CSS architecture, it is difficult to imagine building manual fallbacks in the same way you might for other CSS features. The fallbacks would likely involve duplicating large sections of code, with different selectors to manage cascade layering — or providing a much simpler fallback stylesheet.

Query feature support using @supports

There is a @supports feature in CSS that will allow authors to test for support of @layer and other at-rules:

@supports at-rule(@layer) {   /* code applied for browsers with layer support */ }  @supports not at-rule(@layer) {   /* fallback applied for browsers without layer support */ }

However, it’s also not clear when this query itself will be supported in browsers.

Assigning layers in HTML with the <link> tag

There is no official specification yet for a syntax to layer entire stylesheets from the html <link> tag, but there is a proposal being developed. That proposal includes a new layer attribute which can be used to assign the styles to a named or anonymous layer:

<!-- styles imported into to the <layer-name> layer --> <link rel="stylesheet" href="example.css" layer="<layer-name>">  <!-- styles imported into to a new anonymous layer --> <link rel="stylesheet" href="example.css" layer>

However, old browsers without support for the layer attribute will ignore it completely, and continue to load the stylesheet without any layering. The results could be pretty unexpected. So the proposal also extends the existing media attribute, so that it allows feature support queries in a support() function.

That would allow us to make layered links conditional, based on support for layering:

<link rel="stylesheet" layer="bootstrap" media="supports(at-rule(@layer))" href="bootstrap.css">

Potential polyfills and workarounds

The major browsers have all moved to an “evergreen” model with updates pushed to users on a fairly short release cycle. Even Safari regularly releases new features in “patch” updates between their more rare-seeming major versions.

That means we can expect browser support for these features to ramp up very quickly. For many of us, it may be reasonable to start using layers in only a few months, without much concern for old browsers.

For others, it may take longer to feel comfortable with the native browser support. There are many other ways to manage the cascade, using selectors, custom properties, and other tools. It’s also theoretically possible to mimic (or polyfill) the basic behavior. There are people working on that polyfill, but it’s not clear when that will be ready either.

More resources

CSS Cascade Layers is still evolving but there is already a lot of resources, including documentation, articles, videos, and demos to help you get even more familiar with layers and how they work.





A Complete Guide to CSS Cascade Layers originally published on CSS-Tricks. You should get the newsletter.


, , ,

Don’t Fight the Cascade, Control It!

If you’re disciplined and make use of the inheritance that the CSS cascade provides, you’ll end up writing less CSS. But because our styles often comes from all kinds of sources — and can be a pain to structure and maintain—the cascade can be a source of frustration, and the reason we end up with more CSS than necessary.

Some years ago, Harry Roberts came up with ITCSS and it’s a clever way of structuring CSS.

Mixed with BEM, ITCSS has become a popular way that people write and organize CSS.

However, even with ITCSS and BEM, there are still times where we still struggle with the cascade. For example, I’m sure you’ve had to @import external CSS components at a specific location to prevent breaking things, or reach for the dreaded !important at some point in time.

Recently, some new tools were added to our CSS toolbox, and they allow us to finally control the cascade. Let’s look at them.

O cascade, :where art thou?

Using the :where pseudo-selector allows us to remove specificity to “just after the user-agent default styles,” no matter where or when the CSS is loaded into the document. That means the specificity of the whole thing is literally zero — totally wiped out. This is handy for generic components, which we’ll look into in a moment.

First, imagine some generic <table> styles, using :where:

:where(table) {   background-color: tan; }

Now, if you add some other table styles before the :where selector, like this:

table {   background-color: hotpink; }  :where(table) {   background-color: tan; }

…the table background becomes hotpink, even though the table selector is specified before the :where selector in the cascade. That’s the beauty of :where, and why it’s already being used for CSS resets.

:where has a sibling, which has almost the exact opposite effect: the :is selector.

The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is(). Selectors Level 4 specification

Expanding on our previous example:

:is(table) {   --tbl-bgc: orange; } table {   --tbl-bgc: tan; } :where(table) {   --tbl-bgc: hotpink;   background-color: var(--tbl-bgc); }

The <table class="c-tbl"> background color will be tan because the specificity of :is is less specific than table.

However, if we were to change it to this:

:is(table, .c-tbl) {   --tbl-bgc: orange; }

…the background color will be orange, since :is has the weight of it’s heaviest selector, which is .c-tbl.

Example: A configurable table component

Now, let’s see how we can use :where in our components. We’ll be building a table component, starting with the HTML:

Let’s wrap .c-tbl in a :where-selector and, just for fun, add rounded corners to the table. That means we need border-collapse: separate, as we can’t use border-radius on table cells when the table is using border-collapse: collapse:

:where(.c-tbl) {   border-collapse: separate;   border-spacing: 0;   table-layout: auto;   width: 99.9%; }

The cells use different styling for the <thead> and <tbody>-cells:

:where(.c-tbl thead th) {   background-color: hsl(200, 60%, 40%);   border-style: solid;   border-block-start-width: 0;   border-inline-end-width: 1px;   border-block-end-width: 0;   border-inline-start-width: 0;   color: hsl(200, 60%, 99%);   padding-block: 1.25ch;   padding-inline: 2ch;   text-transform: uppercase; } :where(.c-tbl tbody td) {   background-color: #FFF;   border-color: hsl(200, 60%, 80%);   border-style: solid;   border-block-start-width: 0;   border-inline-end-width: 1px;   border-block-end-width: 1px;   border-inline-start-width: 0;   padding-block: 1.25ch;   padding-inline: 2ch; }

And, because of our rounded corners and the missing border-collapse: collapse, we need to add some extra styles, specifically for the table borders and a hover state on the cells:

:where(.c-tbl tr td:first-of-type) {   border-inline-start-width: 1px; } :where(.c-tbl tr th:last-of-type) {   border-inline-color: hsl(200, 60%, 40%); } :where(.c-tbl tr th:first-of-type) {   border-inline-start-color: hsl(200, 60%, 40%); } :where(.c-tbl thead th:first-of-type) {   border-start-start-radius: 0.5rem; } :where(.c-tbl thead th:last-of-type) {   border-start-end-radius: 0.5rem; } :where(.c-tbl tbody tr:last-of-type td:first-of-type) {   border-end-start-radius: 0.5rem; } :where(.c-tbl tr:last-of-type td:last-of-type) {   border-end-end-radius: 0.5rem; } /* hover */ @media (hover: hover) {   :where(.c-tbl) tr:hover td {     background-color: hsl(200, 60%, 95%);   } }

Now we can create variations of our table component by injecting other styles before or after our generic styles (courtesy of the specificity-stripping powers of :where), either by overwriting the .c-tbl element or by adding a BEM-style modifier-class (e.g. c-tbl--purple):

<table class="c-tbl c-tbl--purple">
.c-tbl--purple th {   background-color: hsl(330, 50%, 40%) } .c-tbl--purple td {   border-color: hsl(330, 40%, 80%); } .c-tbl--purple tr th:last-of-type {   border-inline-color: hsl(330, 50%, 40%); } .c-tbl--purple tr th:first-of-type {   border-inline-start-color: hsl(330, 50%, 40%); }

Cool! But notice how we keep repeating colors? And what if we want to change the border-radius or the border-width? That would end up with a lot of repeated CSS.

Let’s move all of these to CSS custom properties and, while we’re at it, we can move all configurable properties to the top of the component’s “scope“ — which is the table element itself — so we can easily play around with them later.

CSS Custom Properties

I’m going to switch things up in the HTML and use a data-component attribute on the table element that can be targeted for styling.

<table data-component="table" id="table">

That data-component will hold the generic styles that we can use on any instance of the component, i.e. the styles the table needs no matter what color variation we apply. The styles for a specific table component instance will be contained in a regular class, using custom properties from the generic component.

[data-component="table"] {   /* Styles needed for all table variations */ } .c-tbl--purple {   /* Styles for the purple variation */ }

If we place all the generic styles in a data-attribute, we can use whatever naming convention we want. This way, we don’t have to worry if your boss insists on naming the table’s classes something like .BIGCORP__TABLE, .table-component or something else.

In the generic component, each CSS property points to a custom property. Properties, that have to work on child-elements, like border-color, are specified at the root of the generic component:

:where([data-component="table"]) {   /* These will will be used multiple times, and in other selectors */   --tbl-hue: 200;   --tbl-sat: 50%;   --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%); }  /* Here, it's used on a child-node: */ :where([data-component="table"] td) {   border-color: var(--tbl-bdc); }

For other properties, decide whether it should have a static value, or be configurable with its own custom property. If you’re using custom properties, remember to define a default value that the table can fall back to in the event that a variation class is missing.

:where([data-component="table"]) {   /* These are optional, with fallbacks */   background-color: var(--tbl-bgc, transparent);   border-collapse: var(--tbl-bdcl, separate); }

If you’re wondering how I’m naming the custom properties, I’m using a component-prefix (e.g. --tbl) followed by an Emmett-abbreviation (e.g. -bgc). In this case, --tbl is the component-prefix, -bgc is the background color, and -bdcl is the border collapse. So, for example, --tbl-bgc is the table component’s background color. I only use this naming convention when working with component properties, as opposed to global properties which I tend to keep more general.

Now, if we open up DevTools, we can play around with the custom properties. For example, We can change --tbl-hue to a different hue value in the HSL color, set --tbl-bdrs: 0 to remove border-radius, and so on.

A :where CSS rule set showing the custom properties of the table showing how the cascade’s specificity scan be used in context.

When working with your own components, this is the point in time you’ll discover which parameters (i.e. the custom property values) the component needs to make things look just right.

We can also use custom properties to control column alignment and width:

:where[data-component="table"] tr > *:nth-of-type(1)) {   text-align: var(--ca1, initial);   width: var(--cw1, initial);   /* repeat for column 2 and 3, or use a SCSS-loop ... */ }

In DevTools, select the table and add these to the element.styles selector:

element.style {   --ca2: center; /* Align second column center */   --ca3: right; /* Align third column right */ }

Now, let’s create our specific component styles, using a regular class, .c-tbl (which stands for “component-table” in BEM parlance). Let’s toss that class in the table markup.

<table class="c-tbl" data-component="table" id="table">

Now, let’s change the --tbl-hue value in the CSS just to see how this works before we start messing around with all of the property values:

.c-tbl {   --tbl-hue: 330; }

Notice, that we only need to update properties rather than writing entirely new CSS! Changing one little property updates the table’s color — no new classes or overriding properties lower in the cascade.

Notice how the border colors change as well. That’s because all the colors in the table inherit from the --tbl-hue variable

We can write a more complex selector, but still update a single property, to get something like zebra-striping:

.c-tbl tr:nth-child(even) td {   --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%); }

And remember: It doesn’t matter where you load the class. Because our generic styles are using :where, the specificity is wiped out, and any custom styles for a specific variation will be applied no matter where they are used. That’s the beauty of using :where to take control of the cascade!

And best of all, we can create all kinds of table components from the generic styles with a few lines of CSS.

Purple table with zebra-striped columns
Light table with a “noinlineborder” parameter… which we’ll cover next

Adding parameters with another data-attribute

So far, so good! The generic table component is very simple. But what if it requires something more akin to real parameters? Perhaps for things like:

  • zebra-striped rows and columns
  • a sticky header and sticky column
  • hover-state options, such as hover row, hover cell, hover column

We could simply add BEM-style modifier classes, but we can actually accomplish it more efficiently by adding another data-attribute to the mix. Perhaps a data-param that holds the parameters like this:

<table data-component="table" data-param="zebrarow stickyrow">

Then, in our CSS, we can use an attribute-selector to match a whole word in a list of parameters. For example, zebra-striped rows:

[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td {   --tbl-td-bgc: var(--tbl-zebra-bgc); }

Or zebra-striping columns:

[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) {   --tbl-td-bgc: var(--tbl-zebra-bgc); }

Let’s go nuts and make both the table header and the first column sticky:

 [data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child {   --tbl-td-bgc: var(--tbl-zebra-bgc);   inset-inline-start: 0;   position: sticky; } [data-component="table"][data-param~="stickyrow"] thead th {   inset-block-start: -1px;   position: sticky; }

Here’s a demo that allows you to change one parameter at a time:

The default light theme in the demo is this:

.c-tbl--light {   --tbl-bdrs: 0;   --tbl-sat: 15%;   --tbl-th-bgc: #eee;   --tbl-th-bdc: #eee;   --tbl-th-c: #555;   --tbl-th-tt: normal; }

…where data-param is set to noinlineborder which corresponds to these styles:

[data-param~="noinlineborder"] thead tr > th {   border-block-start-width: 0;   border-inline-end-width: 0;   border-block-end-width: var(--tbl-bdw);   border-inline-start-width: 0; }

I know my data-attribute way of styling and configuring generic components is very opinionated. That’s just how I roll, so please feel free to stick with whatever method you’re most comfortable working with, whether it’s a BEM modifier class or something else.

The bottom line is this: embrace :where and :is and the cascade-controlling powers they provide. And, if possible, construct the CSS in such a way that you wind up writing as little new CSS as possible when creating new component variations!

Cascade Layers

The last cascade-busting tool I want to look at is “Cascade Layers.” At the time of this writing, it’s an experimental feature defined in the CSS Cascading and Inheritance Level 5 specification that you can access in Safari or Chrome by enabling the #enable-cascade-layers flag.

Bramus Van Damme sums up the concept nicely:

The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

Perhaps even nicer is his illustration showing where Cascade Layers fall in the cascade:

Credit: Bramus Van Damme

At the beginning of this article, I mentioned ITCSS — a way of taming the cascade by specifying the load-order of generic styles, components etc. Cascade Layers allow us to inject a stylesheet at a given location. So a simplified version of this structure in Cascade Layers looks like this:

@layer generic, components;

With this single line, we’ve decided the order of our layers. First come the generic styles, followed by the component-specific ones.

Let’s pretend that we’re loading our generic styles somewhere much later than our component styles:

@layer components {   body {     background-color: lightseagreen;   } }  /* MUCH, much later... */  @layer generic {    body {     background-color: tomato;   } }

The background-color will be lightseagreen because our component styles layer is set after the generic styles layer. So, the styles in the components layer “win” even if they are written before the generic layer styles.

Again, just another tool for controlling how the CSS cascade applies styles, allowing us more flexibility to organize things logically rather than wrestling with specificity.

Now you’re in control!

The whole point here is that the CSS cascade is becoming a lot easier to wrangle, thanks to new features. We saw how the :where and :is pseudo-selectors allows us to control specificity, either by stripping out the specificity of an entire ruleset or taking on the specificity of the most specific argument, respectively. Then we used CSS Custom Properties to override styles without writing a new class to override another. From there, we took a slight detour down data-attribute lane to help us add more flexibility to create component variations merely by adding arguments to the HTML. And, finally, we poked at Cascade Layers which should prove handy for specifying the loading order or styles using @layer.

If you leave with only one takeaway from this article, I hope it’s that the CSS cascade is no longer the enemy it’s often made to be. We are gaining the tools to stop fighting it and start leaning into even more.

Header photo by Stephen Leonardi on Unsplash

Don’t Fight the Cascade, Control It! originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , ,

Cascade Layers?

There is a new thing coming in CSS: @layer. As with all new things, it takes time to really wrap your head around it. And despite me tapping at my keyboard about it, full disclosure, I’m not sure my mind is fully there yet. Fortunately, smart people are on the case!

This comes from Miriam Suzanne, who is really on a tear with influencing important new CSS stuff. I’ve been hearing about all this, but then all the sudden it just dropped in experimental browsers:

I had this really simplistic view of what the plan was with layers. Say you link up two CSS files:

<link rel="stylesheet" href="1.css"> <link rel="stylesheet" href="2.css">

Anything selectors in 2.css will “win” over selectors the same specificity as something in 1.css. I thought CSS layers was a way to control that without needing to change the source order of those stylesheets in HTML. So, no matter where you load a reset stylesheet, you can kinda notch down the importance of it by kicking it to an earlier layer. Or, no matter where you load an overrides stylesheet, you can kick it up.

I think that kind of thing might be coming still, but the actual concept of layers is much more powerful than I first imagined.

Leave it to Bramus to really dig into all this with a fantastic post on it all:

With Cascade Layers coming, we developers will have more tools available to control the Cascade. The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

Bramus Van Damme, “The Future of CSS: Cascade Layers (CSS @layer)

Emphasis mine.

That’s the rub here: this is a new thing that affects which selectors win. It’s going to require some re-wiring of our CSS brains, because layers is this entirely new (and powerful) part of determining what styles actually get applied.

I say powerful because a “higher” layer can literally beat a traditionally stronger selector even with a weaker selector in the layer.

Miriam’s support demo makes this clear in a few lines:

/* layers override unlayered styles */ @layer override {   h1 {     color: green;   }    h1::before {     content: "✅ ";   } }  /* despite the higher specificity */ #h1 {   color: red; }  #h1::before {   content: "❌ "; }

Because that CSS at the bottom isn’t in a layer at all, the layered CSS wins, even with the weaker selectors. Wow.

And you aren’t limited to one layer. You get to define them and use them however you want.

@layer reset;     /* Create 1st layer named “reset” */ @layer base;      /* Create 2nd layer named “base” */ @layer theme;     /* Create 3rd layer named “theme” */ @layer utilities; /* Create 4th layer named “utilities” */ /* Or, @layer reset, base, theme, utilities; */   @layer reset { /* Append to layer named “reset” */   /* ... */ }  @layer theme { /* Append to layer named “theme” */   /* ... */ }  @layer base { /* Append to layer named “base” */   /* ... */ }  @layer theme { /* Append to layer named “theme” */   /* ... */ }

Mind-blowing, really.

How are we going to use this?

I wonder if a common pattern might turn into…

  1. Not layering our resets, so they are extra weak.
  2. Layering third-party imports at a low level.
  3. Layering any authored styles at a high level.

And then not really worrying if you leave space between the layers because you can adjust layers any time.

Time shall tell.

I bet we’ll see people dip their toes with:

/* This is our only layer. Anything in here wins. */ @layer overrides {    /* ... */ }

And then a couple years later:

@layer final {    /* ... */ } @layer final-final {    /* ... */ } @layer final-final-v2 {    /* ... */ } @layer final-final-final-last-complete {    /* ... */ }


I hope DevTools expresses layers really clearly because there is going to be some serious head-scratching for a while when we see weaker-looking selectors winning because of layer placement.

Browser Support

Looks like caniuse is on the ball here!

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.


Chrome Firefox IE Edge Safari
No No No No No

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
No No No No

The post Cascade Layers? appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



Should DevTools teach the CSS cascade?

Stefan Judis, two days before I mouthed off about using (X, X, X, X) for talking about specificity, has a great blog post not only using that format, but advocating that browser DevTools should show us that value by selectors.

I think that the above additions could help to educate developers about CSS tremendously. The only downside I can think of is that additional information might overwhelm developers, but I would take that risk in favor of more people learning CSS properly.

I’d be for it. The crossed-off UI for the “losing” selectors is attempting to teach this, but without actually teaching it. I wouldn’t be that worried about the information being overwhelming. I think if they are considerate about the design, it can be done tastefully. DevTools is a very information-dense place anyway.

Direct Link to ArticlePermalink

The post Should DevTools teach the CSS cascade? appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , ,

Using Custom Property “Stacks” to Tame the Cascade

Since the inception of CSS in 1994, the cascade and inheritance have defined how we design on the web. Both are powerful features but, as authors, we’ve had very little control over how they interact. Selector specificity and source order provide some minimal “layering” control, without a lot of nuance — and inheritance requires an unbroken lineage. Now, CSS Custom Properties allow us to manage and control both cascade and inheritance in new ways.

I want to show you how I’ve used Custom Property “stacks” to solve some of the common issues people face in the cascade: from scoped component styles, to more explicit layering of intents.

A quick intro to Custom Properties

The same way browsers have defined new properties using a vendor prefix like -webkit- or -moz-, we can define our own Custom Properties with an “empty” -- prefix. Like variables in Sass or JavaScript, we can use them to name, store, and retrieve values — but like other properties in CSS, they cascade and inherit with the DOM.

/* Define a custom property */ html {   --brand-color: rebeccapurple; }

In order to access those captured values, we use the var() function. It has two parts: first the name of our custom property, and then a fallback in case that property is undefined:

button {   /* use the --brand-color if available, or fall back to deeppink */   background: var(--brand-color, deeppink); }

This is not a support fallback for old browsers. If a browser doesn’t understand custom properties, it will ignore the entire var() declaration. Instead, this is a built-in way of handling undefined variables, similar to a font stack defining fallback font families when one is unavailable. If we don’t provide a fallback, the default is unset.

Building variable “stacks”

This ability to define a fallback is similar to “font stacks” used on the font-family property. If the first family is unavailable, the second will be used, and so on. The var() function only accepts a single fallback, but we can nest var() functions to create custom-property fallback “stacks” of any size:

button {   /* try Consolas, then Menlo, then Monaco, and finally monospace */   font-family: Consolas, Menlo, Monaco, monospace;    /* try --state, then --button-color, then --brand-color, and finally deeppink */   background: var(--state, var(--button-color, var(--brand-color, deeppink))); }

If that nested syntax for stacked properties looks bulky, you can use a pre-processor like Sass to make it more compact.

That single-fallback limitation is required to support fallbacks with a comma inside them — like font stacks or layered background images:

html {   /* The fallback value is "Helvetica, Arial, sans-serif" */   font-family: var(--my-font, Helvetica, Arial, sans-serif); }

Defining “scope”

CSS selectors allow us to drill down into the HTML DOM tree, and style elements anywhere on the page, or elements in a particular nested context.

/* all links */ a { color: slateblue; }  /* only links inside a section */ section a { color: rebeccapurple; }  /* only links inside an article */ article a { color: deeppink; }

That’s useful, but it doesn’t capture the reality of “modular” object-oriented or component-driven styles. We might have multiple articles and asides, nested in various configurations. We need a way to clarify which context, or scope, should take precedence when they overlap.

Proximity scopes

Let’s say we have a .light theme and a .dark theme. We can use those classes on the root <html> element to define a page-wide default, but we can also apply them to specific components, nested in various ways:

Each time we apply one of our color-mode classes, the background and color properties are reset, then inherited by nested headings and paragraphs. In our main context, colors inherit from the .light class, while the nested heading and paragraph inherit from the .dark class. Inheritance is based on direct lineage, so the nearest ancestor with a defined value will take precedence. We call that proximity.

Proximity matters for inheritance, but it has no impact on selectors, which rely on specificity. That becomes a problem if we want to style something inside the dark or light containers.

Here I’ve attempted to define both light and dark button variants. Light mode buttons should be rebeccapurple with white text so they stand out, and dark mode buttons should be plum with black text. We’re selecting the buttons directly based on a light and dark context, but it doesn’t work:

Some of the buttons are in both contexts, with both .light and .dark ancestors. What we want in that case is for the closest theme to take over (inheritance proximity behavior), but what we get instead is the second selector overriding the first (cascade behavior). Since the two selectors have the same specificity, source order determines the winner.

Custom Properties and proximity

What we need here is a way to inherit these properties from the theme, but only apply them to specific children. Custom Properties make that possible! We can define values on the light and dark containers, while only using their inherited values on nested elements, like our buttons.

We’ll start by setting up the buttons to use custom properties, with a fallback “default” value, in case those properties are undefined:

button {   background: var(--btn-color, rebeccapurple);   color: var(--btn-contrast, white); }

Now we can set those values based on context, and they will scope to the appropriate ancestor based on proximity and inheritance:

.dark {   --btn-color: plum;   --btn-contrast: black; }  .light {   --btn-color: rebeccapurple;   --btn-contrast: white; }

As an added bonus, we’re using less code overall, and one unified button definition:

I think of this as creating an API of available parameters for the button component. Sara Soueidan and Lea Verou have both covered this well in recent articles.

Component ownership

Sometimes proximity isn’t enough to define scope. When JavaScript frameworks generate “scoped styles” they are establishing specific object-element ownership. A “tab layout” component owns the tabs themselves, but not the content behind each tab. This is also what the BEM convention attempts to capture in complex .block__element class names.

Nicole Sullivan coined the term “donut scope” to talk about this problem back in 2011. While I’m sure she has more recent thoughts on the issue, the fundamental problem hasn’t changed. Selectors and specificity are great for describing how we build detailed styles over top of broad patterns, but they don’t convey a clear sense of ownership.

We can use custom property stacks to help solve this problem. We’ll start by creating “global” properties on the <html> element that are for our default colors:

html {   --background--global: white;   --color--global: black;   --btn-color--global: rebeccapurple;   --btn-contrast--global: white; }

That default global theme is now available anywhere we want to refer to it. We’ll do that with a data-theme attribute that applies our foreground and background colors. We want the global values to provide a default fallback, but we also want the option to override with a specific theme. That’s where “stacks” come in:

[data-theme] {   /* If there's no component value, use the global value */   background: var(--background--component, var(--background--global));   color: var(--color--component, var(--color--global)); }

Now we can define an inverted component by setting the *--component properties as a reverse of the global properties:

[data-theme='invert'] {   --background--component: var(--color--global);   --color--component: var(--background--global); }

But we don’t want those settings to inherit beyond the donut of ownership, so we reset those values to initial (undefined) on every theme. We’ll want to do this at a lower specificity, or earlier in the source order, so it provides a default that each theme can override:

[data-theme] {   --background--component: initial;   --color--component: initial; }

The initial keyword has a special meaning when used on custom properties, reverting them to a Guaranteed-Invalid state. That means rather than being passed along to set background: initial or color: initial, the custom property becomes undefined, and we fallback to the next value in our stack, the global settings.

We can do the same thing with our buttons, and then make sure to apply data-theme to each component. If no specific theme is given, each component will default to the global theme:

Defining “origins”

The CSS cascade is a series of filtering layers used to determine what value should take precedence when multiple values are defined on the same property. We most often interact with the specificity layers, or the final layering based on source-order — but the first layer of cascade is the “origin” of a style. The origin describes where a style came from — often the browser (defaults), the user (preferences), or the author (that’s us).

By default, author styles override user preferences, which override browser defaults. That changes when anyone applies `!important` to a style, and the origins reverse: browser `!important` styles have the highest origin, then important user preferences, then our author important styles, above all the normal layers. There are a few additional origins, but we won’t go into them here.

When we create custom property “stacks,” we’re building a very similar behavior. If we wanted to represent existing origins as a stack of custom properties, it would look something like this:

.origins-as-custom-properties {   color: var(--browser-important, var(--user-important, var(--author-important, var(--author, var(--user, var(--browser)))))); }

Those layers already exist, so there’s no reason to recreate them. But we’re doing something very similar when we layer our “global” and “component” styles above — creating a “component” origin layer that overrides our “global” layer. That same approach can be used to solve various layering issues in CSS, which can’t always be described by specificity:

  • Override » Component » Theme » Default
  • Theme » Design system or framework
  • State » Type » Default

Let’s look at some buttons again. We’ll need a default button style, a disabled state, and various button “types,” like danger, primary and secondary. We wan’t the disabled state to always override the type variations, but selectors don’t capture that distinction:

But we can define a stack that provides both “type” and “state” layers in the order that we want them prioritized:

button {   background: var(--btn-state, var(--btn-type, var(--btn-default))); }

Now when we set both variables, the state will always take precedence:

I’ve used this technique to create a Cascading Colors framework that allows custom theming based on layering:

  • Pre-defined theme attributes in the HTML
  • User color preferences
  • Light and dark modes
  • Global theme defaults

Mix and match

These approaches can be taken to an extreme, but most day-to-day use-cases can be handled with two or three values in a stack, often using a combination of the techniques above:

  • A variable stack to define the layers
  • Inheritance to set them based on proximity and scope
  • Careful application of the `initial` value to remove nested elements from a scope

We’ve been using these custom property “stacks” on our projects at OddBird. We’re still discovering as we go, but they’ve already been helpful in solving problems that were difficult using only selectors and specificity. With custom properties, we don’t have to fight the cascade or inheritance. We can capture and leverage them, as-intended, with more control over how they should apply in each instance. To me, that’s a big win for CSS — especially when developing style frameworks, tools, and systems.

The post Using Custom Property “Stacks” to Tame the Cascade appeared first on CSS-Tricks.


, , , , ,

Amelia Wattenberger’s The CSS Cascade

If you’re on a small screen, remind yourself to check it out on a big screen when you have the chance.

Did you know that styles from an active transition beat !important rules, but styles from an active animation do not? I definitely did not.

Or that there are “origins” that are almost like a secret layer of specificity, and are reversed when !important rules are in play? What?!

Oh and speaking of origins, there is discussion (that’s a tweet from Jen about an idea that came from Miriam) about opening them up to having user-authored origin levels. As in, loading Bootstrap at a lower origin level on purpose, and your own styles at a higher origin, so that anything you write wins. I keep going back and forth on it. I love having powerful tools, and this kind of overriding power seems clearly useful, but I don’t like the idea that I can’t look at a selector and know if it’s going to win or not, I need to know information about the origin too, which will be tricky to find out as it’s likely in another file who-knows-where as CSS can be loaded a number of different ways.

Direct Link to ArticlePermalink

The post Amelia Wattenberger’s The CSS Cascade appeared first on CSS-Tricks.


, ,

The “C” in CSS: The Cascade

Following up from Geoff’s intro article on The Second “S” in CSS, let’s now move the spotlight to the “C” in CSS — what we call the Cascade. It’s where things start to get messy, and even confusing at times.

Have you ever written a CSS property and the value doesn’t seem to work? Maybe you had to turn to using !important to get it going. Or perhaps you resorted to writing the CSS inline on the element in the HTML file.

<div style="background:orange; height:100px; width:100px;">   Ack, inline! </div>

Speaking of inline styles, have you wondered why SVG editors use them instead of a separate CSS file? That seems kinda weird, right?

<svg id="icon-logo-star" viewBox="0 0 362.62 388.52" width="100%" height="100%">   <style>     .logo {       fill: #ff9800;     }   </style>   <title>CSS Tricks Logo</title>   <path class="logo" d="M156.58 239l-88.3 64.75c-10.59 7.06-18.84 11.77-29.43 11.77-21.19 0-38.85-18.84-38.85-40 0-17.69 14.13-30.64 27.08-36.52l103.6-44.74-103.6-45.92C13 142.46 0 129.51 0 111.85 0 90.66 18.84 73 40 73c10.6 0 17.66 3.53 28.25 11.77l88.3 64.75-11.74-104.78C141.28 20 157.76 0 181.31 0s40 18.84 36.5 43.56L206 149.52l88.3-64.75C304.93 76.53 313.17 73 323.77 73a39.2 39.2 0 0 1 38.85 38.85c0 18.84-12.95 30.61-27.08 36.5l-103.61 45.91L335.54 239c14.13 5.88 27.08 18.83 27.08 37.67 0 21.19-18.84 38.85-40 38.85-9.42 0-17.66-4.71-28.26-11.77L206 239l11.77 104.78c3.53 24.72-12.95 44.74-36.5 44.74s-40-18.84-36.5-43.56z"></path> </svg>

Well, the cascade has a lot to do with this. Read on to find out how styling methods affect what’s being applied to your elements and how to use the cascade to your advantage because, believe me, it’s a wonderful thing when you get the hang of it.

TL;DR: Jump right to the CSS order diagram for a visual of how everything works.

The cascade cares about how and where styles are written

There are a myriad of ways you can apply CSS rules to an element. Below is an example of how stroke: red; can be applied to the same element. The examples are ordered in ascending priority, where the highest priority is at the bottom:

<!-- Inheritance --> <g style="stroke: red">   <rect x="1" y="1" width="10" height="10" /> <!-- inherits stroke: red --> </g>  <!-- Inline attributes --> <rect x="1" y="1" width="10" height="10" stroke="red" />  <!-- External style sheet --> <link rel="stylesheet" href="/path/to/stylesheet.css">  <!-- Embedded styles --> <style>   rect { stroke: red; } </style>  <!-- Different specificity or selectors --> rect { stroke: red; } .myClass { stroke: red; } #myID { stroke: red; }  <!-- Inline style --> <g style="stroke: red"></g>  <!-- Important keyword --> <g style="stroke: red !important"></g>

Inheritance? Embedded? External? Inline? Specificity? Important? Yeah, lots of terms being thrown around. Let’s break those down a bit because each one determines what the browser ends up using when a web page loads.

Elements can inherit styles from other elements

Both HTML and SVG elements can inherit CSS rules that are applied to other elements. We call this a parent-child relationship, where the element the CSS is applied to is the parent and the element contained inside the parent is the child.

<div class="parent">   <div class="child">I'm the child because the parent is wrapped around me.</div> </div>

If we set the text color of the parent and do not declare a text color on the child, then the child will look up to the parent to know what color its text should be. We call that inheritance and it’s a prime example of how a style cascades down to an element it matches… or “bubbles up” the chain to the next matched style.

However, inheritance has the lowest priority among styling methods. In other words, if a child has a rule that is specific to it, then the inherited value will be ignored, even though the inherited value may have an important keyword. The following is an example:

<div class="parent" style="color: red !important;">   <div class="child">I'm the child because the parent is wrapped around me.</div> </div>

See the Pen Child ignores inline inheritance with !important by Geoff Graham (@geoffgraham) on CodePen.

SVG inline attributes

For SVG elements, we can also apply styles using inline attributes, where those have the second lowest priority in the cascade. This means the CSS rules in a stylesheet will be able to override them.

<rect x="1" y="1" width="10" height="10" stroke="red" />
rect {   stroke: blue; }

See the Pen Stylesheet overrides SVG inline attributes by Geoff Graham (@geoffgraham) on CodePen.

Most SVG editors use inline attributes for portability; that is, the ability to copy some elements and paste them elsewhere without losing the attributes. Users can then use the resultant SVG and style its elements using an external stylesheet.


Stylesheets are divided into two flavors: external and embedded:

<!-- External style sheet --> <link rel="stylesheet" href="/path/to/stylesheet.css">  <!-- Embedded styles --> <style>   div { border: 1px solid red } </style>

Embedded styles have a higher priority than external stylesheets. Therefore, if you have the same CSS rules, those in the embedded style will be applied.

See the Pen Embedded styles override stylesheet rules by Geoff Graham (@geoffgraham) on CodePen.

All stylesheets also follow ordering rules, where files that are defined later, will have higher priority than those defined earlier. In this example, stylesheet-2.css will take precedence over the stylesheet-1.css file because it is defined last.

<link rel="stylesheet" href="/path/to/stylesheet-1.css"> <link rel="stylesheet" href="/path/to/stylesheet-2.css">

Specificity or selectors

How you select your elements will also determine which rules are applied, whereby tags (e.g. <p>, <div>), classes (e.g. .my-class) and IDs (e.g. #myI-id) have ascending priorities.

See the Pen Specificity by selectors by Geoff Graham (@geoffgraham) on CodePen.

In the example above, if you have a div element with both .my-class and #my-id, the border will be red because IDs have higher priority than classes and tags.

*Specificity has higher priority than ordering rules, therefore, irrespective if your rule is at the top or bottom. Specificity still has higher priority and will be applied.


CSS rules always prioritize from left-to-right, then from top-to-bottom.

<!-- Blue will be applied because it is on the right --> <div style="1px solid red; 1px solid blue;"></div>   <style>   div {     border: 1px solid red;     border: 1px solid blue; /* This will be applied because it is at the bottom */   } </style>

Inline styles

Inline styles have the second highest priority, just below the !important keyword. This means that inline styles are only overridden by the important keyword and nothing else. Within inline styles, normal ordering rules applies, from left-to-right and top-to-bottom.

<div style="1px solid red;"></div>

The important keyword

Speaking of the !important keyword, it is used to override ordering, specificity and inline rules. In other words, it wields incredible powers.

Overriding inline rules

<style>   div {     /* This beats inline styling */     border: 1px solid orange !important;     /* These do not */     height: 200px;     width: 200px;   } </style>  <div style="border: 1px solid red; height: 100px; width: 100px;"></div>

In the example above, without the important keyword, the div would have a red border because inline styling has higher priority than embedded styles. But, with the important keyword, the div border becomes orange, because the important keyword has higher priority than inline styling.

Using !important can be super useful, but should be used with caution. Chris has some thoughts on situations where it makes sense to use it.

Overriding specificity rules

Without the important keyword, this div border will be blue, because classes have higher priority than tags in specificity.

<style>   /* Classes have higher priority than tags */   .my-class {     border: 1px solid blue;     height: 100px;     width: 100px;   }      div {      border: 1px solid red;     height: 200px;     width: 200px;   } </style>  <div class="my-class"></div>

See the Pen Classes beat tags by Geoff Graham (@geoffgraham) on CodePen.

But! Adding the important keyword to the tag rules tells the element to ignore the cascade and take precedence over the class rules.

<style>   .my-class { border: 1px solid red; }      /* The important keyword overrides specificity priority */   .my-class { border: 1px solid blue !important; } </style>  <div class="my-class"></div>

See the Pen !important ignores the cascade by Geoff Graham (@geoffgraham) on CodePen.

Overriding ordering rules

OK, so we’ve already talked about how the order of rules affects specificity: bottom beats top and right beats left. The surefire way to override that is to put !important into use once again.

In this example, the div will take the red border, even though the blue border is the bottom rule. You can thank !important for that handiwork.

<style>   div { border: 1px solid red !important; } /* This wins, despite the ordering */   div { border: 1px solid blue; } </style>  <div></div>

See the Pen Important wins over ordering by Geoff Graham (@geoffgraham) on CodePen.

Visualizing the cascade

Who knew there was so much meaning in the “C” of CSS? We covered a ton of ground here and hopefully it helps clarify the way styles are affected and applied by how we write them. The cascade is a powerful feature. There are opinions galore about how to use it properly, but you can see the various ways properties are passed and inherited by elements.

More of a visual learner? Here’s a chart that pulls it all together.

Download chart

The post The “C” in CSS: The Cascade appeared first on CSS-Tricks.