CSS Cascade Layers: ending specificity wars

CSS Cascade Layers (the @layer at-rule) solve a long-standing pain point: specificity wars.

Before layers, overriding a third-party library's highly-specific selector meant writing something even more specific, or reaching for !important.

Layers let us group styles into buckets and rank those buckets directly, independent of selector specificity.

Declaration order decides priority

Layers declared later win, no matter how specific the selectors are inside earlier layers:

@layer reset, base, components;

@layer reset {
  p {
    margin: 0;
  }
}

@layer components {
  p {
    margin: 20px;
  } /* wins: 'components' is declared last */
}

Even #my-id p inside reset would lose to the plain p in components — the whole layer sits higher in the cascade.

Specificity still works as normal inside a layer; layers only decide which bucket beats another bucket.

Unlayered CSS always wins

To avoid breaking existing stylesheets, any rule outside of an @layer block beats layered CSS, regardless of order:

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

.btn {
  background-color: red;
} /* always wins, unlayered */

(!important flips this: important rules inside layers beat important unlayered rules, so component libraries can still force safety overrides.)

Common patterns

Declare the layer order upfront, then fill layers in anywhere in the file (or across files):

@layer theme, layout, components;

@layer components {
  .card {
    padding: 1rem;
  }
}

Wrap third-party CSS on import so it never fights with our own styles:

@import "framework.css" layer(framework);
@import "theme.css" layer(theme);

Layers can also nest, via dot notation:

@layer framework.layout {
  .container {
    max-width: 1200px;
  }
}

Why bother