Using CSS Cascade Layers in Angular

We've all been there: You define a new CSS rule, rebuild your web application, and check the browser to see that your styling isn't being applied. Come on!

Many times, there's a conflict with another CSS rule that has higher specificity. Maybe you've included a third-party library that defines style rules that conflict with your own.

Luckily for us web developers, a new CSS feature comes to the rescue: CSS Cascade Layers. They allow us to strictly layer our styles and avoid conflicts. I'll show you how to use them in an Angular web application with SCSS.

A cake with several visible layers. Photo: © Dalila Dalprat / pexels.com

What are Cascade Layers?

CSS Cascade Layers give authors more control over the cascade, which is to say: The way the browser chooses which styling rules to apply for a specific HTML element.

The new feature is defined in the CSS Module Cascading and Inheritance Level 5. Although the document has only "Editor's Draft" status, cascade layers are already supported by all modern browsers. The document states:

Cascade layers provide a structured way to organize and balance concerns within a single origin. Rules within a single cascade layer cascade together, without interleaving with style rules outside the layer.

This means that we can establish various layers – starting with low-priority styles like resets and third-party libraries, up to high-priority styles like components and utilities. Conflicts between layers are always resolved by using the higher-priority layer styles.

If we look at the steps of the cascade, we see that layers are evaluated before specificity and order of appearance are taken into account:

  1. Origin and importance (!important)
  2. Context
  3. Element Attached styles
  4. Layers
  5. Specificity
  6. Order of Appearance

Please watch the video “The CSS Cascade, a deep dive” by Bramus Van Damme if you want to explore the CSS cascade in more detail.

The @layer rule

In general, we create a named cascade layer and define rules within this layer using the @layer rule:

@layer my-layer { // CSS rules }

You can also create a named cascade layer without assigning any styles. This is used to determine the hierarchy of your layers. A best practice is to first define the hierarchy of your layers in a single @layer statement, followed by the style rules separated into these layers.

@layer my-layer-1, my-layer-2; @layer my-layer-1 { // CSS rules } @layer my-layer-2 { // CSS rules }

Any styles not in a layer are gathered together and placed into a single anonymous layer that comes after all the declared layers. This means that any styles declared outside of a layer will always override styles declared in a layer.

I won't get into all the details about cascade layers. Please read the great article “A Complete Guide to CSS Cascade Layers”.

Demo Application with Angular and SCSS

I've created an application with Angular 14 and Leaflet to display an interactive map. I use SCSS for styling, as it offers great features like placeholders and mixins. Check out my demo code here.

My goal for the application is simple: I want to make sure that my styling for individual components is always applied. They should take precedence over, e.g., CSS resets and library styles. For this purpose, I've created several cascade layers in the main styles.scss file of my application:

@use "sass:meta"; @layer reset, thirdparty, overrides; @layer reset { @include meta.load-css("layers/reset/normalize"); } @layer thirdparty { @include meta.load-css("node_modules/leaflet/dist/leaflet.css"); } @layer overrides { @include meta.load-css("layers/overrides/leaflet"); }

I use the SASS function load-css to load the CSS from another SCSS file and include it as part of a cascade layer. I put all user agent style resets in the lowest-priority layer, followed by the layer for the basic styling of the interactive leaflet map and a layer for global overrides.

I don't define cascade layers for the styles of specific components declared in their own SCSS file. All these styles are automatically put into an anonymous layer that receives the highest priority. Let's look at a specific example.

Styling a Map Popup

My demo app lists my 5 favorite places in Vienna. They are marked on an interactive leaflet map. When you click on a marker, a popup opens. The HTML template fav-place-popup.component.html of the popup component looks like this:

<h2>{{data.name}}</h2> <p *ngFor="let desc of data.description">{{desc}}</p>

I've applied the following styles in fav-place-popup.component.scss:

p { font-size: 0.8rem; margin: 0.5em 0; }

At the same time, the leaflet library defines the following styles for paragraphs inside of map popups:

.leaflet-popup-content p { margin: 18px 0; }

Thanks to my established order of cascade layers, I don't have to worry about leaflet's styles for .leaflet-popup-content p interfering with my component styles for p. At the same time, Angular's view encapsulation ensures that my component's styles are not applied to all paragraphs in my application. Sweet!

Conclusion

I love cascade layers! They're a powerful tool to structure your styles and avoid pesky conflicts due to specificity or order of appearance.

There's one problem remaining: Some Angular libraries (e.g. Angular Material) automatically add styles to the HTML document's head tag. So far, I haven't found a way to put these styles in a cascade layer of my choice. I hope this issue will be addressed in the future.

Posted on