components icon indicating copy to clipboard operation
components copied to clipboard

feat(all): Support for CSS Cascade Layers

Open johanndev opened this issue 2 years ago • 13 comments
trafficstars

Feature Description

Are there any plans to introduce support for CSS Cascade Layers?

References: https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_layers https://caniuse.com/css-cascade-layers https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/

Use Case

To meet the demands of our customers, our team often has to override the default styles of angular material components. We'd like to use cascade layers to organize our normalization, override, and custom styles. If the angular components would be wrapped in a dedicated (configurable?) layer, our material overrides could be embedded more conveniently into our overall style logic.

johanndev avatar Jan 18 '23 09:01 johanndev

I'd very much appreciate this too! The problem right now is that many Angular Material components add CSS rules inside a style tag to the document's head. Currently there is no way to configure that these CSS rules should be defined as part of a specific CSS cascade layer.

I would also like to offer my help in implementing this feature.

alexlehner86 avatar Jan 18 '23 10:01 alexlehner86

Any update on this?

alexlehner86 avatar Jun 27 '23 05:06 alexlehner86

Can I be of assistance with this? Has anyone already worked on this, or do you guys have a specific design in mind?

Opcyc-Ben avatar Jan 23 '24 11:01 Opcyc-Ben

@OpCyc-Ben As far as I know, nobody has worked on this yet. Here's a suggestion how it could work:

  • Developers using Angular Material in an Angular application, can provide the name for a dedicated cascade layer via an InjectionToken.
  • If the layer name is provided, then all Material components wrap their styles in this cascade layer using the @layer block at-rule.
  • If developers don't want to use a cascade layer for Angular Material components, then they simply don't provide the layer name. In this case, the components would provide the styles as is, without wrapping them in a cascade layer block.
  • The documentation for the cascade layer option should explain that the developers must define the order of the cascade layers in their project, e.g., by using @layer followed by the names of the layers without assigning any styles. It's possible that this order definition has to be done in the first style tag in the head section of the index.html, but I'm not sure. Maybe it's sufficient to do it in the (S)CSS files that are bundled into the styles.css file. This would need some experimentation.

What do you think?

alexlehner86 avatar Jan 23 '24 11:01 alexlehner86

@alexlehner86 While I love that idea, I'd like to first discuss first what we are conceptually trying to do. I'm even torn whether angular itself might be a better place for this. To be transparent about my motivation, our (my) use-case for this would be to wrap any non-layered styles into a base-layer. This use-case would go beyond the scope of 'material' or angular's own components.

  • As far as I understand style isolation seems to attempt to keep intact whatever styles a component author sets.
  • ::ng-deep allows you to explicitly override styles for all children of a given selector even protected ones, by adding whatever css that requires, at the level you need it.
  • Adding @layer, it seems to me, would have to be more like an app-wide configuration change, as the emitted style tag this would have to end up in isn't repeated for every instance of every component they apply to.

My open questions to you would be: Where exactly do you envision that InjectionToken to go, and what exactly do you think it should do to the output? What's your use-case?

Opcyc-Ben avatar Jan 23 '24 14:01 Opcyc-Ben

With Tailwind v4 leaning heavily into CSS cascade layers, it's becoming vital for other libraries (like this one) to support them too. Right now I'm having to resort to marking all Tailwind utilities as !important just so they can take precedence when used on Angular Material components. Having the styles for Angular Material components placed in a (user defined) layer would avoid having this kind of hack in place.

jits avatar Feb 10 '25 08:02 jits

@alexlehner86 While I love that idea, I'd like to first discuss first what we are conceptually trying to do. I'm even torn whether angular itself might be a better place for this. To be transparent about my motivation, our (my) use-case for this would be to wrap any non-layered styles into a base-layer. This use-case would go beyond the scope of 'material' or angular's own components.

* As far as I understand style isolation seems to attempt to keep intact whatever styles a component author sets.

* ::ng-deep allows you to explicitly override styles for all **children** of a given selector even protected ones, by adding whatever css that requires, at the level you need it.

* Adding `@layer`, it seems to me, would have to be more like an app-wide configuration change, as the emitted style tag this would have to end up in isn't repeated for every instance of every component they apply to.

My open questions to you would be: Where exactly do you envision that InjectionToken to go, and what exactly do you think it should do to the output? What's your use-case?

Maybe there was a misunderstanding. What I would like to be able to do is: Tell Angular Material to put all CSS styles for the Material components into a named cascade layer. This would make it easier to override their styling if necessary.

I described a possible solution using an InjectionToken. There are other possibilities as well. I'm open to suggestions 😊

alexlehner86 avatar Feb 10 '25 09:02 alexlehner86

Absence of layers is basically making Tailwind4 conflicting with Angular material. Simpe example: <mat-icon class="text-red-50" fontIcon="fa-cog" fontSet="fal" />

Expected: tailwind class will set red color to the icon Actual: default mat-icon class has higher specificity because all the tailwind4 classes are inside layers

DzmVasileusky avatar Feb 13 '25 10:02 DzmVasileusky

temporary workaround

Concept

  1. create an observable that
    • create MutationObserver that observes new additions to an element, and passes them to the observable stream
    • takes the current list of node inside the document head, and pass them to the stream (this is the initial batch of streamed items)
    • start observing the document head with the MutationObserver
    • at cleanup, disconnect the MutationObserver
  2. filter the observable to only handle HTMLStyleElements
  3. wrap the contents of the stylesheet inside a @layer directive, if we can verify that it is a material component stylesheet

Note: for apps that also do SSR, avoid running this on the server-side, I guess. As for SSG, I "think" this code will work fine even for that, because we do process the existing style tags too at the moment the observable starts up.

How to detect a material component stylesheet

  1. style element has no attributes
  2. stylesheet starts with character sequence that is relevant to material
    • ".mat-progress-spinner" for spinner, ".mat-sidenav" for sidenav, etc
    • currently, I just use ".mat-" and ".cdk-", because it matches all
    • if you find during development, some stylesheet is not getting wrapped, just take the first few characters you think are appropriate and add them to the list of prefixes that are being checked

Note: you can use this trick to wrap other stylesheets as well, I guess - just add the starting sequence of characters to the prefix list to check


Implementation

This implementation is for rxjs using effects, but you can easily adapt it for use inside your top-level app-component, or wherever else you like

wrapLibraryStylesInLayer: createEffect(
    () => {
      // skip this effect during ssr
      if (isServer())
        return EMPTY;

      const document = inject(DOCUMENT);
      const prefixes = ['.mat-', '.cdk-'];

      function wrapContents(styleElement: HTMLStyleElement) {
        if (styleElement.attributes.length !== 0)
          return;

        const styleContent = styleElement.textContent || '';

        if (
          !styleContent ||
          !prefixes.some(prefix => styleContent.startsWith(prefix))
        )
          return;

        console.debug(styleElement);

        styleElement.textContent = `@layer components { ${styleContent} }`;
      }

      const mutations$ = new Observable<Node>(subscriber => {
        const observer = new MutationObserver(mutations => {
          mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => subscriber.next(node));
          });
        });

        document.head.childNodes.forEach(node => subscriber.next(node));

        observer.observe(document.head, { childList: true });

        return () => observer.disconnect();
      });

      return mutations$
        .pipe(
          takeUntilDestroyed(),
          filter(node => node instanceof HTMLStyleElement),
          tap(node => wrapContents(node))
        );
    },
    { functional: true, dispatch: false }
  )

DibyodyutiMondal avatar Mar 29 '25 23:03 DibyodyutiMondal

the list of prefixes can be updated to

      const prefixes = [
        '.mat-',
        '.mdc-',
        '.cdk-',
        'div.mat-' // mat-autocomplete
      ];

DibyodyutiMondal avatar Mar 29 '25 23:03 DibyodyutiMondal

I decided to add a completely separate layer for angular material. This has to be done before we import tailwindcss, in our main style file

Image

thene we can update the code:

-styleElement.textContent = `@layer components { ${styleContent} }`;
+styleElement.textContent = `@layer material { ${styleContent} }`;

DibyodyutiMondal avatar Mar 30 '25 00:03 DibyodyutiMondal

You will also have to update the code that initializes the material theme

Image

DibyodyutiMondal avatar Mar 30 '25 01:03 DibyodyutiMondal

@DibyodyutiMondal Thanks for sharing your workaround with the mutation observer. I'll have to give that a try!

@Opcyc-Ben Still, I would prefer for Angular Material to support cascade layer wrapping per default. Is there anything I can do to help with the implementation?

alexlehner86 avatar May 18 '25 10:05 alexlehner86

Hello guys, it’s been a few years since this issue was created and I belive this is of great importance. Probably if more developers were aware of CSS Layers and their power, this ticket would probably have many more upvotes.

I would like to leave some notes on the subject hoping to motivate the conversation around the subject matter and discuss how we could solve this.


⭐ Why Angular Material should consider supporting CSS Layers

1️⃣ CSS Layers (@layer) provide an explicit mechanism for defining and controlling the cascade order of styles. This is very important for a design system like Angular Material because:

  • Predictable overrides: It helps developers understand exactly how component styles from Angular Material can be overridden without relying on brittle specificity hacks (e.g., !important or selectors with deep specificity).
  • Isolation of library styles: Angular Material could define its theme and core component styles in a dedicated @layer, giving downstream consumers a clear, controlled way to apply their custom styles “above” it in the cascade.
@layer angular-material, my-overrides;

@import "@angular/material/theming.css" layer(angular-material);

/* Custom styles safely layered above */
@layer my-overrides {
  .mat-button {
    background-color: red;
  }
}

or for those using custom themes

@use '@angular/material/core/theming/all-theme';
@use '@angular/material/core/core';
@use '@angular/material/core/m2';
@use '@angular/material/core/typography/typography';

@layer components {
     @import './palette/success';

     @include core.app-background();
     @include core.elevation-classes();

     $primary: m2.define-palette(m2.$indigo-palette);
     $accent: m2.define-palette(m2.$blue-palette, A200, A100, A400);
     $success: m2.define-palette($success-palette);

     $theme: m2.define-light-theme(
          (
               color: (
                    primary: $primary,
                    accent: $accent,
               ),
               typography: m2.define-typography-config(),
               density: 0,
          )
     );

     @include all-theme.all-component-themes($theme);

     @include typography.typography-hierarchy($theme);
}

2️⃣ Simpler Customization Without Shadow DOM

  • Angular Material currently avoids Shadow DOM to allow styling from outside the component boundary, but this means the framework relies heavily on selectors and theming APIs.
  • CSS Layers would allow Angular Material to maintain this openness while reducing unintended side-effects:
  • Consumers could author overrides without wrestling Angular Material’s internal selector specificity.
  • The framework itself could make guarantees about default styling behavior, reducing regressions.

3️⃣ Alignment with Platform Standards

  • CSS Layers are a native platform feature, so supporting them:
  • Future-proofs Angular Material against a rapidly evolving CSS ecosystem.
  • Reduces reliance on proprietary theming APIs or documentation-heavy style guides for customization.
  • Offers an elegant, declarative API surface that complements Angular Material’s emphasis on developer ergonomics.

4️⃣ Facilitating Interoperability with Other Libraries Many organizations combine Angular Material with other component libraries or legacy styles. CSS Layers would enable:

  • Clear separation and ordering of third-party styles.
  • Easier adoption alongside utility-first frameworks (like Tailwind CSS) that may also leverage layers.

Best regards 0/

mreis1 avatar Jul 21 '25 13:07 mreis1