From e19c55f803b2ea0dd3107920bc6957690863220c Mon Sep 17 00:00:00 2001 From: Cory Seaman Date: Mon, 30 Mar 2026 16:17:47 -0400 Subject: [PATCH 1/2] feat(material/core): add optional CSS cascade layers for mat.theme - Add core/style cascade-layers API: with-cascade-layer mixin and default angular-material layer name - Extend mat.theme with optional third argument cascade-layer (default null) - Document layer ordering with CDK (cdk-overlay, cdk-resets) in theming guide - Cross-reference overlay-structure.scss and add sass/unit tests Related to #26451 Made-with: Cursor --- guides/theming.md | 55 +++++++++++++ src/cdk/overlay/overlay-structure.scss | 3 + src/material/BUILD.bazel | 2 + src/material/_index.scss | 2 + src/material/core/style/BUILD.bazel | 5 ++ src/material/core/style/_cascade-layers.scss | 21 +++++ src/material/core/theming/tests/BUILD.bazel | 8 ++ .../core/theming/tests/cascade-layers.spec.ts | 81 +++++++++++++++++++ .../theming/tests/test-cascade-layers.scss | 18 +++++ src/material/core/tokens/BUILD.bazel | 1 + src/material/core/tokens/_system.scss | 14 +++- 11 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/material/core/style/_cascade-layers.scss create mode 100644 src/material/core/theming/tests/cascade-layers.spec.ts create mode 100644 src/material/core/theming/tests/test-cascade-layers.scss diff --git a/guides/theming.md b/guides/theming.md index c9610de48f1b..c653859eea9b 100644 --- a/guides/theming.md +++ b/guides/theming.md @@ -48,6 +48,61 @@ html { } ``` +### CSS cascade layers + +You can place `mat.theme` output in a +[CSS cascade layer](https://www.w3.org/TR/css-cascade-5/#layering) so normalization, +Angular CDK/Material, and app overrides stay predictable. For background, see +[MDN: Cascade layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer). + +**Layer order.** Declare the order of named layers (including +[from CDK](https://github.com/angular/components/blob/main/src/cdk/overlay/_index.scss) +`cdk-overlay` and `cdk-resets` when you use those packages) before other rules, for example: + +```scss +@layer reset, cdk-resets, cdk-overlay, angular-material, overrides; +``` + +**Default is unlayered.** `mat.theme` does not use a layer unless you opt in. + +Use either the optional third argument: + +```scss +@use '@angular/material' as mat; + +html { + @include mat.theme( + ( + color: mat.$violet-palette, + typography: Roboto, + density: 0, + ), + (), + mat.$default-cascade-layer-name + ); +} +``` + +or wrap the mixin with `with-cascade-layer` and a custom name: + +```scss +@include mat.with-cascade-layer(custom-material) { + @include mat.theme(( + color: mat.$violet-palette, + typography: Roboto, + density: 0, + )); +} +``` + +**Cascade note.** In the CSS Cascade Level 5 model, unlayered author declarations participate in an +implicit outer layer with higher priority than explicit named layers. Unlayered rules can therefore +override layered Material at equal specificity. Overrides that live in named layers must appear after +your Material layer in the prelude `@layer` list (or use higher specificity / `!important` as usual). + +Per-component styles from Angular Material are emitted in separate stylesheets and are not placed +in this layer by default; this feature applies to the system variables emitted by `mat.theme`. + You can use the following styles to apply the theme’s surface background and on-surface text colors as a default across your application: diff --git a/src/cdk/overlay/overlay-structure.scss b/src/cdk/overlay/overlay-structure.scss index ab1fc6573daf..dccd483943b8 100644 --- a/src/cdk/overlay/overlay-structure.scss +++ b/src/cdk/overlay/overlay-structure.scss @@ -2,6 +2,9 @@ // We don't emit the layer internally, because all the breaking changes // have been resolved already and the `@layer` seems to break some targets. +// Published CSS still uses `@layer cdk-overlay`. Apps combining CDK with +// Material can prelude layer order alongside `mat.with-cascade-layer` / +// `mat.theme(..., $cascade-layer: ...)` (see `guides/theming.md`). $_is-external-build: true; @include overlay.private-overlay-structure($_is-external-build); diff --git a/src/material/BUILD.bazel b/src/material/BUILD.bazel index 4640784669a3..22bea3c42e8f 100644 --- a/src/material/BUILD.bazel +++ b/src/material/BUILD.bazel @@ -54,6 +54,7 @@ sass_library( "//src/material/core/selection/pseudo-checkbox:_pseudo_checkbox_common", "//src/material/core/selection/pseudo-checkbox:sass_theme", "//src/material/core/style:_validation", + "//src/material/core/style:cascade_layers", "//src/material/core/style:elevation", "//src/material/core/style:private", "//src/material/core/style:sass_utils", @@ -130,6 +131,7 @@ ng_package( "//src/material/core/selection/pseudo-checkbox:_pseudo_checkbox_common", "//src/material/core/selection/pseudo-checkbox:sass_theme", "//src/material/core/style:_validation", + "//src/material/core/style:cascade_layers", "//src/material/core/style:elevation", "//src/material/core/style:private", "//src/material/core/style:sass_utils", diff --git a/src/material/_index.scss b/src/material/_index.scss index dfdf3738c594..a535539a8cdf 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -33,6 +33,8 @@ $private-ease-in-out-curve-function, $private-swift-ease-out-duration, $private-xsmall; @forward './core/style/sass-utils' as private-*; @forward './core/style/validation' as private-*; +@forward './core/style/cascade-layers' show $default-cascade-layer-name, + with-cascade-layer; // Structural @forward './core/core' show core, app-background, elevation-classes; diff --git a/src/material/core/style/BUILD.bazel b/src/material/core/style/BUILD.bazel index 959dc5a7c22f..895ca3083288 100644 --- a/src/material/core/style/BUILD.bazel +++ b/src/material/core/style/BUILD.bazel @@ -67,6 +67,11 @@ sass_library( srcs = ["_sass-utils.scss"], ) +sass_library( + name = "cascade_layers", + srcs = ["_cascade-layers.scss"], +) + sass_library( name = "_validation", srcs = ["_validation.scss"], diff --git a/src/material/core/style/_cascade-layers.scss b/src/material/core/style/_cascade-layers.scss new file mode 100644 index 000000000000..53a459ce3a1c --- /dev/null +++ b/src/material/core/style/_cascade-layers.scss @@ -0,0 +1,21 @@ +// Cascade layers for Angular Material (CSS Cascade Module Level 5). +// Compose with app-authored @layer ordering. CDK also uses layers in published CSS +// (`cdk-overlay` for overlay structural styles, `cdk-resets` for drag-drop); declare +// layer order once, e.g. @layer reset, cdk-resets, cdk-overlay, angular-material, +// overrides; + +/// Default layer name for Material theme output when using cascade-layer helpers. +$default-cascade-layer-name: angular-material !default; + +/// Wraps emitted CSS in a named cascade [@layer](https://www.w3.org/TR/css-cascade-5/#layering). +/// +/// Unlayered author styles sort after explicit layers in the cascade, so they can override +/// rules inside this layer without higher specificity. For overrides in named layers, declare +/// those layers after `angular-material` (or your chosen name) in a prelude `@layer` rule. +/// +/// @param {String} $name Layer name (may use nested syntax such as `app.material`). +@mixin with-cascade-layer($name: $default-cascade-layer-name) { + @layer #{$name} { + @content; + } +} diff --git a/src/material/core/theming/tests/BUILD.bazel b/src/material/core/theming/tests/BUILD.bazel index 49dd08d74b0b..f1232e84dbf3 100644 --- a/src/material/core/theming/tests/BUILD.bazel +++ b/src/material/core/theming/tests/BUILD.bazel @@ -52,6 +52,13 @@ sass_binary( deps = ["//src/material:sass_lib"], ) +sass_binary( + name = "test-cascade-layers", + testonly = True, + src = "test-cascade-layers.scss", + deps = ["//src/material:sass_lib"], +) + build_test( name = "sass_compile_tests", targets = [ @@ -59,6 +66,7 @@ build_test( ":test-theming-api", ":test-theming-bundle", ":test-typography-font-family", + ":test-cascade-layers", ], ) diff --git a/src/material/core/theming/tests/cascade-layers.spec.ts b/src/material/core/theming/tests/cascade-layers.spec.ts new file mode 100644 index 000000000000..9a827fd455bd --- /dev/null +++ b/src/material/core/theming/tests/cascade-layers.spec.ts @@ -0,0 +1,81 @@ +import {runfiles} from '@bazel/runfiles'; +import * as path from 'path'; +import {compileString} from 'sass'; + +import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer.js'; + +const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests'); +const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..'); +const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir); + +function transpile(content: string) { + return compileString( + ` + @use '../../../index' as mat; + + $theme: mat.define-theme(); + + ${content} + `, + { + loadPaths: [testDir], + importers: [localPackageSassImporter], + }, + ).css.toString(); +} + +describe('CSS cascade layers', () => { + it('wraps mat.theme in @layer when third argument is set', () => { + const css = transpile(` + html { + @include mat.theme( + ( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + ), + (), + mat.$default-cascade-layer-name + ); + } + `); + expect(css).toContain('@layer angular-material'); + }); + + it('does not emit @layer when third argument is omitted', () => { + const css = transpile(` + html { + @include mat.theme(( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + )); + } + `); + expect(css).not.toContain('@layer'); + }); + + it('wraps output for with-cascade-layer', () => { + const css = transpile(` + html { + @include mat.with-cascade-layer(custom-layer) { + @include mat.theme(( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + )); + } + } + `); + expect(css).toContain('@layer custom-layer'); + }); +}); diff --git a/src/material/core/theming/tests/test-cascade-layers.scss b/src/material/core/theming/tests/test-cascade-layers.scss new file mode 100644 index 000000000000..31c3200e8702 --- /dev/null +++ b/src/material/core/theming/tests/test-cascade-layers.scss @@ -0,0 +1,18 @@ +@use '../../../index' as mat; + +$theme: mat.define-theme(); + +html { + @include mat.theme( + ( + color: ( + theme-type: light, + primary: mat.$violet-palette, + ), + typography: Roboto, + density: 0, + ), + (), + mat.$default-cascade-layer-name + ); +} diff --git a/src/material/core/tokens/BUILD.bazel b/src/material/core/tokens/BUILD.bazel index 9c454a93e3a9..fa8eea93fc8b 100644 --- a/src/material/core/tokens/BUILD.bazel +++ b/src/material/core/tokens/BUILD.bazel @@ -24,6 +24,7 @@ sass_library( srcs = ["_system.scss"], deps = [ ":m3_tokens", + "//src/material/core/style:cascade_layers", "//src/material/core/style:elevation", "//src/material/core/style:sass_utils", "//src/material/core/theming:_config_validation", diff --git a/src/material/core/tokens/_system.scss b/src/material/core/tokens/_system.scss index 7cc7cd463dc4..eb8aaf3c793a 100644 --- a/src/material/core/tokens/_system.scss +++ b/src/material/core/tokens/_system.scss @@ -15,6 +15,7 @@ @use '../../tabs/m3-tabs'; @use '../../toolbar/m3-toolbar'; @use '../../tree/m3-tree'; +@use '../style/cascade-layers' as cascade-layers; @use '../style/elevation'; @use '../theming/config-validation'; @use '../theming/definition'; @@ -54,7 +55,18 @@ /// e.g. --mat-sys-surface: #E5E5E5 /// /// @param {Map} $config The color configuration with optional keys color, typography, or density. -@mixin theme($config, $overrides: ()) { +/// @param {String | null} $cascade-layer When non-null, wraps output in [@layer](https://www.w3.org/TR/css-cascade-5/#layering). +@mixin theme($config, $overrides: (), $cascade-layer: null) { + @if ($cascade-layer == null) { + @include _theme-impl($config, $overrides); + } @else { + @include cascade-layers.with-cascade-layer($cascade-layer) { + @include _theme-impl($config, $overrides); + } + } +} + +@mixin _theme-impl($config, $overrides: ()) { $color: map.get($config, color); $color-config: null; @if ($color) { From 194ea217ea00b8ea6111688d4769a248e79bb683 Mon Sep 17 00:00:00 2001 From: Cory Seaman Date: Mon, 30 Mar 2026 19:08:43 -0400 Subject: [PATCH 2/2] layer shipped CSS for predictable app overrides --- guides/theming.md | 80 +++++++++++-------- src/cdk/overlay/overlay-structure.scss | 3 - src/e2e-app/BUILD.bazel | 1 + src/e2e-app/components/e2e-app/e2e-app.ts | 1 + src/e2e-app/components/tailwind-layer-e2e.ts | 31 +++++++ src/e2e-app/main.ts | 2 + src/e2e-app/theme.scss | 21 ++++- src/material-experimental/menubar/BUILD.bazel | 2 + .../selection/BUILD.bazel | 1 + src/material/_index.scss | 3 +- src/material/autocomplete/BUILD.bazel | 1 + src/material/badge/BUILD.bazel | 1 + src/material/bottom-sheet/BUILD.bazel | 1 + src/material/button-toggle/BUILD.bazel | 1 + src/material/button/BUILD.bazel | 4 + src/material/card/BUILD.bazel | 1 + src/material/checkbox/BUILD.bazel | 1 + src/material/chips/BUILD.bazel | 2 + src/material/core/BUILD.bazel | 1 + .../core/focus-indicators/BUILD.bazel | 1 + .../core/internal-form-field/BUILD.bazel | 1 + src/material/core/option/BUILD.bazel | 2 + .../selection/pseudo-checkbox/BUILD.bazel | 1 + src/material/core/style/_cascade-layers.scss | 25 +++--- .../core/theming/prebuilt/BUILD.bazel | 8 ++ src/material/core/theming/tests/BUILD.bazel | 2 + .../core/theming/tests/cascade-layers.spec.ts | 50 ++++++------ .../theming/tests/test-cascade-layers.scss | 12 ++- src/material/core/tokens/BUILD.bazel | 1 - src/material/core/tokens/_system.scss | 14 +--- src/material/datepicker/BUILD.bazel | 6 ++ src/material/dialog/BUILD.bazel | 1 + src/material/divider/BUILD.bazel | 1 + src/material/expansion/BUILD.bazel | 2 + src/material/form-field/BUILD.bazel | 1 + src/material/grid-list/BUILD.bazel | 1 + src/material/icon/BUILD.bazel | 1 + src/material/list/BUILD.bazel | 2 + src/material/menu/BUILD.bazel | 1 + src/material/paginator/BUILD.bazel | 1 + src/material/progress-bar/BUILD.bazel | 1 + src/material/progress-spinner/BUILD.bazel | 1 + src/material/radio/BUILD.bazel | 1 + src/material/schematics/ng-add/index.spec.ts | 2 + .../schematics/ng-add/theming/create-theme.ts | 35 ++++---- src/material/select/BUILD.bazel | 1 + src/material/sidenav/BUILD.bazel | 1 + src/material/slide-toggle/BUILD.bazel | 1 + src/material/slider/BUILD.bazel | 2 + .../slider/tailwind-layer.e2e.spec.ts | 35 ++++++++ src/material/snack-bar/BUILD.bazel | 2 + src/material/sort/BUILD.bazel | 1 + src/material/stepper/BUILD.bazel | 2 + src/material/table/BUILD.bazel | 1 + src/material/tabs/BUILD.bazel | 5 ++ src/material/timepicker/BUILD.bazel | 1 + src/material/toolbar/BUILD.bazel | 1 + src/material/tooltip/BUILD.bazel | 1 + src/material/tree/BUILD.bazel | 1 + tools/defaults.bzl | 66 ++++++++++++--- 60 files changed, 323 insertions(+), 130 deletions(-) create mode 100644 src/e2e-app/components/tailwind-layer-e2e.ts create mode 100644 src/material/slider/tailwind-layer.e2e.spec.ts diff --git a/guides/theming.md b/guides/theming.md index c653859eea9b..038f87981c2f 100644 --- a/guides/theming.md +++ b/guides/theming.md @@ -50,58 +50,70 @@ html { ### CSS cascade layers -You can place `mat.theme` output in a -[CSS cascade layer](https://www.w3.org/TR/css-cascade-5/#layering) so normalization, -Angular CDK/Material, and app overrides stay predictable. For background, see +Angular Material ships all component CSS inside the `angular-material` +[cascade layer](https://www.w3.org/TR/css-cascade-5/#layering). This lets +applications control where Material sits in the cascade relative to resets, +CDK styles, and utility frameworks such as Tailwind — without resorting to +extra specificity or `!important`. See also [MDN: Cascade layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer). -**Layer order.** Declare the order of named layers (including -[from CDK](https://github.com/angular/components/blob/main/src/cdk/overlay/_index.scss) -`cdk-overlay` and `cdk-resets` when you use those packages) before other rules, for example: +Component styles are layered automatically at build time — no action is +needed for component CSS. Prebuilt themes are also shipped pre-layered. + +**Layer order.** In Sass, `@use` must come before any other rules. After your +`@use` lines, declare the order of named layers: ```scss -@layer reset, cdk-resets, cdk-overlay, angular-material, overrides; +@use '@angular/material' as mat; + +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; ``` -**Default is unlayered.** `mat.theme` does not use a layer unless you opt in. +If your app uses CDK overlay or drag-drop resets, include their published +layer names (`cdk-overlay`, `cdk-resets`) so their priority is predictable. -Use either the optional third argument: +**Tailwind CSS.** Placing `angular-material` before `utilities` allows +Tailwind utility classes to override Material styles at equal specificity. +Put Material `@use` and your layer prelude before Tailwind’s `@tailwind` +directives (Tailwind’s own docs describe layer setup for your stack): ```scss @use '@angular/material' as mat; -html { - @include mat.theme( - ( - color: mat.$violet-palette, - typography: Roboto, - density: 0, - ), - (), - mat.$default-cascade-layer-name - ); -} +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; + +@tailwind base; +@tailwind components; +@tailwind utilities; ``` -or wrap the mixin with `with-cascade-layer` and a custom name: +**Theme output.** Wrap `mat.theme` in `mat.theme-layer` so the generated +CSS custom properties land in the same `angular-material` layer as the +component styles. You can also use the same mixin around other Angular +Material theme output such as `all-component-themes` or individual +component theme mixins: ```scss -@include mat.with-cascade-layer(custom-material) { - @include mat.theme(( - color: mat.$violet-palette, - typography: Roboto, - density: 0, - )); +@use '@angular/material' as mat; + +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; + +@include mat.theme-layer { + html { + @include mat.theme(( + color: mat.$violet-palette, + typography: Roboto, + density: 0, + )); + } } ``` -**Cascade note.** In the CSS Cascade Level 5 model, unlayered author declarations participate in an -implicit outer layer with higher priority than explicit named layers. Unlayered rules can therefore -override layered Material at equal specificity. Overrides that live in named layers must appear after -your Material layer in the prelude `@layer` list (or use higher specificity / `!important` as usual). - -Per-component styles from Angular Material are emitted in separate stylesheets and are not placed -in this layer by default; this feature applies to the system variables emitted by `mat.theme`. +**Cascade note.** In the CSS Cascade Level 5 model, unlayered author +styles have higher priority than any named layer. Plain CSS rules can +therefore override layered Material at equal specificity. Overrides that +live inside named layers must appear after `angular-material` in the +`@layer` order list, or use higher specificity / `!important` as usual. You can use the following styles to apply the theme’s surface background and on-surface text colors as a default across your application: diff --git a/src/cdk/overlay/overlay-structure.scss b/src/cdk/overlay/overlay-structure.scss index dccd483943b8..ab1fc6573daf 100644 --- a/src/cdk/overlay/overlay-structure.scss +++ b/src/cdk/overlay/overlay-structure.scss @@ -2,9 +2,6 @@ // We don't emit the layer internally, because all the breaking changes // have been resolved already and the `@layer` seems to break some targets. -// Published CSS still uses `@layer cdk-overlay`. Apps combining CDK with -// Material can prelude layer order alongside `mat.with-cascade-layer` / -// `mat.theme(..., $cascade-layer: ...)` (see `guides/theming.md`). $_is-external-build: true; @include overlay.private-overlay-structure($_is-external-build); diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel index 51e091c66d12..75637beae19f 100644 --- a/src/e2e-app/BUILD.bazel +++ b/src/e2e-app/BUILD.bazel @@ -43,6 +43,7 @@ ng_project( "//src/cdk/scrolling", "//src/cdk/testing/tests:test_components", "//src/components-examples/private", + "//src/material/button", "//src/material/core", "//src/material/slider", ], diff --git a/src/e2e-app/components/e2e-app/e2e-app.ts b/src/e2e-app/components/e2e-app/e2e-app.ts index 581b9550607b..79c49b98a9f7 100644 --- a/src/e2e-app/components/e2e-app/e2e-app.ts +++ b/src/e2e-app/components/e2e-app/e2e-app.ts @@ -16,6 +16,7 @@ export class E2eApp { {path: 'block-scroll-strategy', title: 'Block Scroll Strategy'}, {path: 'component-harness', title: 'Component Harness'}, {path: 'slider', title: 'Slider'}, + {path: 'tailwind-layer', title: 'Tailwind Layer'}, {path: 'virtual-scroll', title: 'Virtual Scroll'}, ]; } diff --git a/src/e2e-app/components/tailwind-layer-e2e.ts b/src/e2e-app/components/tailwind-layer-e2e.ts new file mode 100644 index 000000000000..1618e9e12fab --- /dev/null +++ b/src/e2e-app/components/tailwind-layer-e2e.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; + +@Component({ + selector: 'tailwind-layer-e2e', + template: ` + + + `, + imports: [MatButtonModule], + changeDetection: ChangeDetectionStrategy.Eager, +}) +export class TailwindLayerE2e {} diff --git a/src/e2e-app/main.ts b/src/e2e-app/main.ts index ee0d1e790bc5..dbd41ae89adb 100644 --- a/src/e2e-app/main.ts +++ b/src/e2e-app/main.ts @@ -7,6 +7,7 @@ import {ComponentHarnessE2e} from './components/component-harness-e2e'; import {E2eApp} from './components/e2e-app/e2e-app'; import {Home} from './components/home'; import {SliderE2e} from './components/slider-e2e'; +import {TailwindLayerE2e} from './components/tailwind-layer-e2e'; import {VirtualScrollE2E} from './components/virtual-scroll/virtual-scroll-e2e'; enableProdMode(); @@ -19,6 +20,7 @@ bootstrapApplication(E2eApp, { {path: 'block-scroll-strategy', component: BlockScrollStrategyE2E}, {path: 'component-harness', component: ComponentHarnessE2e}, {path: 'slider', component: SliderE2e}, + {path: 'tailwind-layer', component: TailwindLayerE2e}, {path: 'virtual-scroll', component: VirtualScrollE2E}, ]), provideZoneChangeDetection(), diff --git a/src/e2e-app/theme.scss b/src/e2e-app/theme.scss index d794f4750e68..f22617945611 100644 --- a/src/e2e-app/theme.scss +++ b/src/e2e-app/theme.scss @@ -1,5 +1,7 @@ @use '@angular/material' as mat; +@layer base, angular-material, utilities; + $theme: mat.define-theme(( color: ( theme-type: light, @@ -11,8 +13,21 @@ $theme: mat.define-theme(( ) )); -html { - @include mat.all-component-themes($theme); +@include mat.theme-layer { + html { + @include mat.all-component-themes($theme); + } + + @include mat.typography-hierarchy($theme); } -@include mat.typography-hierarchy($theme); +@layer utilities { + .tw-bg-lime-500 { + background-color: rgb(132, 204, 22); + } +} + +// Control utility that is intentionally unlayered. +.tw-bg-fuchsia-unlayered { + background-color: rgb(217, 70, 239); +} diff --git a/src/material-experimental/menubar/BUILD.bazel b/src/material-experimental/menubar/BUILD.bazel index 8556339b26a2..6a90bd59f45f 100644 --- a/src/material-experimental/menubar/BUILD.bazel +++ b/src/material-experimental/menubar/BUILD.bazel @@ -32,11 +32,13 @@ sass_library( sass_binary( name = "menubar_scss", src = "menubar.scss", + layer = "angular-material", ) sass_binary( name = "menubar_item_scss", src = "menubar-item.scss", + layer = "angular-material", ) ng_project( diff --git a/src/material-experimental/selection/BUILD.bazel b/src/material-experimental/selection/BUILD.bazel index 2cc414f9276d..15cec99036e3 100644 --- a/src/material-experimental/selection/BUILD.bazel +++ b/src/material-experimental/selection/BUILD.bazel @@ -27,4 +27,5 @@ sass_library( sass_binary( name = "selection_column_scss", src = "selection-column.scss", + layer = "angular-material", ) diff --git a/src/material/_index.scss b/src/material/_index.scss index a535539a8cdf..76fa9ab9061e 100644 --- a/src/material/_index.scss +++ b/src/material/_index.scss @@ -33,8 +33,7 @@ $private-ease-in-out-curve-function, $private-swift-ease-out-duration, $private-xsmall; @forward './core/style/sass-utils' as private-*; @forward './core/style/validation' as private-*; -@forward './core/style/cascade-layers' show $default-cascade-layer-name, - with-cascade-layer; +@forward './core/style/cascade-layers' show theme-layer; // Structural @forward './core/core' show core, app-background, elevation-classes; diff --git a/src/material/autocomplete/BUILD.bazel b/src/material/autocomplete/BUILD.bazel index 44dea63a861f..482d8f6cebc9 100644 --- a/src/material/autocomplete/BUILD.bazel +++ b/src/material/autocomplete/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "autocomplete.scss", + layer = "angular-material", deps = [ ":m2", ":m3", diff --git a/src/material/badge/BUILD.bazel b/src/material/badge/BUILD.bazel index 2943c37a634a..0a9cef7d097f 100644 --- a/src/material/badge/BUILD.bazel +++ b/src/material/badge/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "badge_css", src = "badge.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/bottom-sheet/BUILD.bazel b/src/material/bottom-sheet/BUILD.bazel index e7c5e6a32b10..14b69fe72d09 100644 --- a/src/material/bottom-sheet/BUILD.bazel +++ b/src/material/bottom-sheet/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "bottom-sheet-container.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/button-toggle/BUILD.bazel b/src/material/button-toggle/BUILD.bazel index ef5093e44c5a..0f0beb307cc9 100644 --- a/src/material/button-toggle/BUILD.bazel +++ b/src/material/button-toggle/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "button-toggle.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/button/BUILD.bazel b/src/material/button/BUILD.bazel index 9671cabd541c..3bb1e6bf510b 100644 --- a/src/material/button/BUILD.bazel +++ b/src/material/button/BUILD.bazel @@ -84,6 +84,7 @@ sass_library( sass_binary( name = "icon_button_css", src = "icon-button.scss", + layer = "angular-material", deps = [ ":base_lib", ":m2", @@ -96,6 +97,7 @@ sass_binary( sass_binary( name = "fab_css", src = "fab.scss", + layer = "angular-material", deps = [ ":base_lib", ":m2", @@ -109,6 +111,7 @@ sass_binary( sass_binary( name = "button_high_contrast", src = "button-high-contrast.scss", + layer = "angular-material", deps = [ "//src/cdk:sass_lib", ], @@ -128,6 +131,7 @@ sass_library( sass_binary( name = "css", src = "button.scss", + layer = "angular-material", deps = [ ":base_lib", ":m2", diff --git a/src/material/card/BUILD.bazel b/src/material/card/BUILD.bazel index de75a8684e6a..20d2793c5954 100644 --- a/src/material/card/BUILD.bazel +++ b/src/material/card/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "card.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/src/material/checkbox/BUILD.bazel b/src/material/checkbox/BUILD.bazel index 5eb1f4c0f456..8476190c1abd 100644 --- a/src/material/checkbox/BUILD.bazel +++ b/src/material/checkbox/BUILD.bazel @@ -64,6 +64,7 @@ sass_library( sass_binary( name = "css", src = "checkbox.scss", + layer = "angular-material", deps = [ ":checkbox_common", ":m2", diff --git a/src/material/chips/BUILD.bazel b/src/material/chips/BUILD.bazel index 0146ffcfaa3e..b4e9553979b5 100644 --- a/src/material/chips/BUILD.bazel +++ b/src/material/chips/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "chip_css", src = "chip.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -65,6 +66,7 @@ sass_binary( sass_binary( name = "chip_set_css", src = "chip-set.scss", + layer = "angular-material", deps = ["//src/material/core/style:vendor_prefixes"], ) diff --git a/src/material/core/BUILD.bazel b/src/material/core/BUILD.bazel index 87aaf7d74341..2d5b9e44b727 100644 --- a/src/material/core/BUILD.bazel +++ b/src/material/core/BUILD.bazel @@ -203,6 +203,7 @@ sass_library( sass_binary( name = "ripple_structure", src = "ripple/ripple-structure.scss", + layer = "angular-material", deps = [ ":ripple_sass", ], diff --git a/src/material/core/focus-indicators/BUILD.bazel b/src/material/core/focus-indicators/BUILD.bazel index 17495d4bbdaf..c8ddaa5b997c 100644 --- a/src/material/core/focus-indicators/BUILD.bazel +++ b/src/material/core/focus-indicators/BUILD.bazel @@ -25,5 +25,6 @@ ng_project( sass_binary( name = "structural_styles_css", src = "structural-styles.scss", + layer = "angular-material", deps = [":focus-indicators"], ) diff --git a/src/material/core/internal-form-field/BUILD.bazel b/src/material/core/internal-form-field/BUILD.bazel index 032b2f748a53..cab3abc55c8d 100644 --- a/src/material/core/internal-form-field/BUILD.bazel +++ b/src/material/core/internal-form-field/BUILD.bazel @@ -16,5 +16,6 @@ ng_project( sass_binary( name = "internal_form_field_css", src = "internal-form-field.scss", + layer = "angular-material", deps = ["//src/material/core/style:vendor_prefixes"], ) diff --git a/src/material/core/option/BUILD.bazel b/src/material/core/option/BUILD.bazel index db193878f52d..ffd610e153b3 100644 --- a/src/material/core/option/BUILD.bazel +++ b/src/material/core/option/BUILD.bazel @@ -77,6 +77,7 @@ ng_project( sass_binary( name = "option_css", src = "option.scss", + layer = "angular-material", deps = [ "//src/cdk:sass_lib", "//src/material/core/style:layout_common", @@ -88,6 +89,7 @@ sass_binary( sass_binary( name = "optgroup_css", src = "optgroup.scss", + layer = "angular-material", deps = [ "//src/material/core/tokens:token_utils", ], diff --git a/src/material/core/selection/pseudo-checkbox/BUILD.bazel b/src/material/core/selection/pseudo-checkbox/BUILD.bazel index 744a69374472..cc3fe7923ab5 100644 --- a/src/material/core/selection/pseudo-checkbox/BUILD.bazel +++ b/src/material/core/selection/pseudo-checkbox/BUILD.bazel @@ -66,6 +66,7 @@ sass_library( sass_binary( name = "css", src = "pseudo-checkbox.scss", + layer = "angular-material", deps = [ ":_pseudo_checkbox_common", "//src/material/core/style:checkbox_common", diff --git a/src/material/core/style/_cascade-layers.scss b/src/material/core/style/_cascade-layers.scss index 53a459ce3a1c..7b21a6f9f4ba 100644 --- a/src/material/core/style/_cascade-layers.scss +++ b/src/material/core/style/_cascade-layers.scss @@ -1,21 +1,14 @@ -// Cascade layers for Angular Material (CSS Cascade Module Level 5). -// Compose with app-authored @layer ordering. CDK also uses layers in published CSS -// (`cdk-overlay` for overlay structural styles, `cdk-resets` for drag-drop); declare -// layer order once, e.g. @layer reset, cdk-resets, cdk-overlay, angular-material, -// overrides; +// Angular Material uses a fixed layer name so shipped component CSS and Sass-emitted theme +// output can participate in the same app-authored @layer ordering. +$private-layer-name: angular-material; -/// Default layer name for Material theme output when using cascade-layer helpers. -$default-cascade-layer-name: angular-material !default; - -/// Wraps emitted CSS in a named cascade [@layer](https://www.w3.org/TR/css-cascade-5/#layering). -/// -/// Unlayered author styles sort after explicit layers in the cascade, so they can override -/// rules inside this layer without higher specificity. For overrides in named layers, declare -/// those layers after `angular-material` (or your chosen name) in a prelude `@layer` rule. +/// Wraps emitted CSS in Angular Material's cascade layer. /// -/// @param {String} $name Layer name (may use nested syntax such as `app.material`). -@mixin with-cascade-layer($name: $default-cascade-layer-name) { - @layer #{$name} { +/// Use this around Angular Material theme mixins such as `mat.theme`, +/// `mat.all-component-themes`, or individual component theme mixins when you want their +/// output to align with the layer used by Angular Material's shipped component CSS. +@mixin theme-layer { + @layer #{$private-layer-name} { @content; } } diff --git a/src/material/core/theming/prebuilt/BUILD.bazel b/src/material/core/theming/prebuilt/BUILD.bazel index f631700a2031..eb3158f6061e 100644 --- a/src/material/core/theming/prebuilt/BUILD.bazel +++ b/src/material/core/theming/prebuilt/BUILD.bazel @@ -5,6 +5,7 @@ package(default_visibility = ["//visibility:public"]) sass_binary( name = "indigo-pink-theme", src = "indigo-pink.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -16,6 +17,7 @@ sass_binary( sass_binary( name = "deeppurple-amber-theme", src = "deeppurple-amber.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -27,6 +29,7 @@ sass_binary( sass_binary( name = "pink-bluegrey-theme", src = "pink-bluegrey.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -38,6 +41,7 @@ sass_binary( sass_binary( name = "purple-green-theme", src = "purple-green.scss", + layer = "angular-material", deps = [ "//src/material/core:core_sass", "//src/material/core/m2:m2_sass", @@ -49,6 +53,7 @@ sass_binary( sass_binary( name = "rose_red", src = "rose-red.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", @@ -58,6 +63,7 @@ sass_binary( sass_binary( name = "azure_blue", src = "azure-blue.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", @@ -67,6 +73,7 @@ sass_binary( sass_binary( name = "cyan_orange", src = "cyan-orange.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", @@ -76,6 +83,7 @@ sass_binary( sass_binary( name = "magenta_violet", src = "magenta-violet.scss", + layer = "angular-material", deps = [ "//src/material/core/theming:_palettes", "//src/material/core/tokens:system", diff --git a/src/material/core/theming/tests/BUILD.bazel b/src/material/core/theming/tests/BUILD.bazel index f1232e84dbf3..1b52996817ba 100644 --- a/src/material/core/theming/tests/BUILD.bazel +++ b/src/material/core/theming/tests/BUILD.bazel @@ -91,6 +91,8 @@ jasmine_test( ":unit_test_lib", "//src/material:sass_lib", "//src/material-experimental:sass_lib", + "//src/material/button:css", + "//src/material/prebuilt-themes:azure-blue", ], no_copy_to_bin = [ "//src/material:sass_lib", diff --git a/src/material/core/theming/tests/cascade-layers.spec.ts b/src/material/core/theming/tests/cascade-layers.spec.ts index 9a827fd455bd..935cd2172469 100644 --- a/src/material/core/theming/tests/cascade-layers.spec.ts +++ b/src/material/core/theming/tests/cascade-layers.spec.ts @@ -1,4 +1,5 @@ import {runfiles} from '@bazel/runfiles'; +import * as fs from 'fs'; import * as path from 'path'; import {compileString} from 'sass'; @@ -25,27 +26,25 @@ function transpile(content: string) { } describe('CSS cascade layers', () => { - it('wraps mat.theme in @layer when third argument is set', () => { + it('wraps mat.theme with mat.theme-layer', () => { const css = transpile(` - html { - @include mat.theme( - ( + @include mat.theme-layer { + html { + @include mat.theme(( color: ( theme-type: light, primary: mat.$violet-palette, ), typography: Roboto, density: 0, - ), - (), - mat.$default-cascade-layer-name - ); + )); + } } `); expect(css).toContain('@layer angular-material'); }); - it('does not emit @layer when third argument is omitted', () => { + it('does not emit @layer when theme-layer is omitted', () => { const css = transpile(` html { @include mat.theme(( @@ -58,24 +57,23 @@ describe('CSS cascade layers', () => { )); } `); - expect(css).not.toContain('@layer'); + expect(css).not.toContain('@layer angular-material'); }); - it('wraps output for with-cascade-layer', () => { - const css = transpile(` - html { - @include mat.with-cascade-layer(custom-layer) { - @include mat.theme(( - color: ( - theme-type: light, - primary: mat.$violet-palette, - ), - typography: Roboto, - density: 0, - )); - } - } - `); - expect(css).toContain('@layer custom-layer'); + it('wraps compiled component CSS in the angular-material layer', () => { + const css = fs.readFileSync( + runfiles.resolveWorkspaceRelative('src/material/button/button.css'), + 'utf8', + ); + expect(css).toMatch(/^@layer angular-material \{/); + expect(css).toContain('.mat-mdc-button-base'); + }); + + it('wraps prebuilt theme CSS in the angular-material layer', () => { + const css = fs.readFileSync( + runfiles.resolveWorkspaceRelative('src/material/prebuilt-themes/azure-blue.css'), + 'utf8', + ); + expect(css).toMatch(/^@layer angular-material \{/); }); }); diff --git a/src/material/core/theming/tests/test-cascade-layers.scss b/src/material/core/theming/tests/test-cascade-layers.scss index 31c3200e8702..4cd6fe7b4975 100644 --- a/src/material/core/theming/tests/test-cascade-layers.scss +++ b/src/material/core/theming/tests/test-cascade-layers.scss @@ -2,17 +2,15 @@ $theme: mat.define-theme(); -html { - @include mat.theme( - ( +@include mat.theme-layer { + html { + @include mat.theme(( color: ( theme-type: light, primary: mat.$violet-palette, ), typography: Roboto, density: 0, - ), - (), - mat.$default-cascade-layer-name - ); + )); + } } diff --git a/src/material/core/tokens/BUILD.bazel b/src/material/core/tokens/BUILD.bazel index fa8eea93fc8b..9c454a93e3a9 100644 --- a/src/material/core/tokens/BUILD.bazel +++ b/src/material/core/tokens/BUILD.bazel @@ -24,7 +24,6 @@ sass_library( srcs = ["_system.scss"], deps = [ ":m3_tokens", - "//src/material/core/style:cascade_layers", "//src/material/core/style:elevation", "//src/material/core/style:sass_utils", "//src/material/core/theming:_config_validation", diff --git a/src/material/core/tokens/_system.scss b/src/material/core/tokens/_system.scss index eb8aaf3c793a..7cc7cd463dc4 100644 --- a/src/material/core/tokens/_system.scss +++ b/src/material/core/tokens/_system.scss @@ -15,7 +15,6 @@ @use '../../tabs/m3-tabs'; @use '../../toolbar/m3-toolbar'; @use '../../tree/m3-tree'; -@use '../style/cascade-layers' as cascade-layers; @use '../style/elevation'; @use '../theming/config-validation'; @use '../theming/definition'; @@ -55,18 +54,7 @@ /// e.g. --mat-sys-surface: #E5E5E5 /// /// @param {Map} $config The color configuration with optional keys color, typography, or density. -/// @param {String | null} $cascade-layer When non-null, wraps output in [@layer](https://www.w3.org/TR/css-cascade-5/#layering). -@mixin theme($config, $overrides: (), $cascade-layer: null) { - @if ($cascade-layer == null) { - @include _theme-impl($config, $overrides); - } @else { - @include cascade-layers.with-cascade-layer($cascade-layer) { - @include _theme-impl($config, $overrides); - } - } -} - -@mixin _theme-impl($config, $overrides: ()) { +@mixin theme($config, $overrides: ()) { $color: map.get($config, color); $color-config: null; @if ($color) { diff --git a/src/material/datepicker/BUILD.bazel b/src/material/datepicker/BUILD.bazel index af9de2eed2e7..62da920a6365 100644 --- a/src/material/datepicker/BUILD.bazel +++ b/src/material/datepicker/BUILD.bazel @@ -63,6 +63,7 @@ sass_library( sass_binary( name = "datepicker_content_css", src = "datepicker-content.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", @@ -72,6 +73,7 @@ sass_binary( sass_binary( name = "calendar_css", src = "calendar.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -83,6 +85,7 @@ sass_binary( sass_binary( name = "calendar_body_css", src = "calendar-body.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -94,6 +97,7 @@ sass_binary( sass_binary( name = "datepicker_toggle_css", src = "datepicker-toggle.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -104,11 +108,13 @@ sass_binary( sass_binary( name = "datepicker_actions_css", src = "datepicker-actions.scss", + layer = "angular-material", ) sass_binary( name = "date_range_input_css", src = "date-range-input.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/dialog/BUILD.bazel b/src/material/dialog/BUILD.bazel index fdeb092c536a..5a2c1e6fc8bc 100644 --- a/src/material/dialog/BUILD.bazel +++ b/src/material/dialog/BUILD.bazel @@ -60,6 +60,7 @@ sass_library( sass_binary( name = "css", src = "dialog.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/divider/BUILD.bazel b/src/material/divider/BUILD.bazel index 30ef52c68430..4c7d8009ea50 100644 --- a/src/material/divider/BUILD.bazel +++ b/src/material/divider/BUILD.bazel @@ -55,6 +55,7 @@ sass_library( sass_binary( name = "css", src = "divider.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/src/material/expansion/BUILD.bazel b/src/material/expansion/BUILD.bazel index 7c3ad0c29e93..f092626f0c01 100644 --- a/src/material/expansion/BUILD.bazel +++ b/src/material/expansion/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "expansion-panel.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -65,6 +66,7 @@ sass_binary( sass_binary( name = "header_css", src = "expansion-panel-header.scss", + layer = "angular-material", deps = [ ":m2", ":variables", diff --git a/src/material/form-field/BUILD.bazel b/src/material/form-field/BUILD.bazel index 9d2bbecd71f1..69e494e19fdd 100644 --- a/src/material/form-field/BUILD.bazel +++ b/src/material/form-field/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "form-field.scss", + layer = "angular-material", deps = [ ":form_field_partials", "//src/material/core/style:vendor_prefixes", diff --git a/src/material/grid-list/BUILD.bazel b/src/material/grid-list/BUILD.bazel index 7e32331c766d..cd285ba24405 100644 --- a/src/material/grid-list/BUILD.bazel +++ b/src/material/grid-list/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "grid-list.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/style:layout_common", diff --git a/src/material/icon/BUILD.bazel b/src/material/icon/BUILD.bazel index 200496244769..c571a00d94af 100644 --- a/src/material/icon/BUILD.bazel +++ b/src/material/icon/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "icon.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/style:vendor_prefixes", diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel index 7f574e3e9b66..b18f8fe99a0b 100644 --- a/src/material/list/BUILD.bazel +++ b/src/material/list/BUILD.bazel @@ -55,6 +55,7 @@ sass_library( sass_binary( name = "css", src = "list.scss", + layer = "angular-material", deps = [ ":list_inherited_structure", ":list_item_hcm_indicator", @@ -83,6 +84,7 @@ sass_library( sass_binary( name = "option_css", src = "list-option.scss", + layer = "angular-material", deps = [ ":list_item_hcm_indicator", "//src/material/checkbox:checkbox_common", diff --git a/src/material/menu/BUILD.bazel b/src/material/menu/BUILD.bazel index 9b8ab63420f1..039b139fb79b 100644 --- a/src/material/menu/BUILD.bazel +++ b/src/material/menu/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "menu.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/paginator/BUILD.bazel b/src/material/paginator/BUILD.bazel index a685da54f71e..9185c5511a3f 100644 --- a/src/material/paginator/BUILD.bazel +++ b/src/material/paginator/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "paginator.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/progress-bar/BUILD.bazel b/src/material/progress-bar/BUILD.bazel index 7cb4f1c4b4e0..3f43b0dd1860 100644 --- a/src/material/progress-bar/BUILD.bazel +++ b/src/material/progress-bar/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "progress-bar.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/progress-spinner/BUILD.bazel b/src/material/progress-spinner/BUILD.bazel index d079b0679711..495609ceb435 100644 --- a/src/material/progress-spinner/BUILD.bazel +++ b/src/material/progress-spinner/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "progress-spinner.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/radio/BUILD.bazel b/src/material/radio/BUILD.bazel index 72a82b87f39e..f280f3e9248c 100644 --- a/src/material/radio/BUILD.bazel +++ b/src/material/radio/BUILD.bazel @@ -63,6 +63,7 @@ sass_library( sass_binary( name = "css", src = "radio.scss", + layer = "angular-material", deps = [ ":m2", ":radio_common", diff --git a/src/material/schematics/ng-add/index.spec.ts b/src/material/schematics/ng-add/index.spec.ts index 58977ad01a32..0db6f472183f 100644 --- a/src/material/schematics/ng-add/index.spec.ts +++ b/src/material/schematics/ng-add/index.spec.ts @@ -107,6 +107,8 @@ describe('ng-add schematic', () => { expect(themeContent).toContain(`@use '@angular/material' as mat;`); expect(themeContent).toContain(`@include mat.theme((`); + expect(themeContent).toContain(`@layer base, cdk-resets, cdk-overlay, angular-material`); + expect(themeContent).toContain(`@include mat.theme-layer`); }); it('should create a custom theme file if no SCSS file could be found', async () => { diff --git a/src/material/schematics/ng-add/theming/create-theme.ts b/src/material/schematics/ng-add/theming/create-theme.ts index fb25eb8eb010..70555a145f92 100644 --- a/src/material/schematics/ng-add/theming/create-theme.ts +++ b/src/material/schematics/ng-add/theming/create-theme.ts @@ -16,22 +16,29 @@ export function createTheme(userPaletteChoice: string): string { ]); return ` // Include theming for Angular Material with \`mat.theme()\`. -// This Sass mixin will define CSS variables that are used for styling Angular Material -// components according to the Material 3 design spec. -// Learn more about theming and how to use it for your application's -// custom components at https://material.angular.dev/guide/theming +// \`@use\` must come before any other rules (including \`@layer\`). @use '@angular/material' as mat; -html { - height: 100%; - @include mat.theme(( - color: ( - primary: mat.$${colorPalettes.get(userPaletteChoice)!.primary}-palette, - tertiary: mat.$${colorPalettes.get(userPaletteChoice)!.tertiary}-palette, - ), - typography: Roboto, - density: 0, - )); +// Cascade layer ordering. Angular Material component styles ship in the +// \`angular-material\` layer. Declaring layer order here makes overrides +// predictable alongside CDK styles and utility frameworks. +// Learn more: https://material.angular.dev/guide/theming#css-cascade-layers +@layer base, cdk-resets, cdk-overlay, angular-material, components, utilities; + +// Wrapping \`mat.theme()\` in \`mat.theme-layer\` places generated CSS +// custom properties in the same \`angular-material\` layer as component styles. +@include mat.theme-layer { + html { + height: 100%; + @include mat.theme(( + color: ( + primary: mat.$${colorPalettes.get(userPaletteChoice)!.primary}-palette, + tertiary: mat.$${colorPalettes.get(userPaletteChoice)!.tertiary}-palette, + ), + typography: Roboto, + density: 0, + )); + } } body { diff --git a/src/material/select/BUILD.bazel b/src/material/select/BUILD.bazel index a0322c99128f..6930fd7daad7 100644 --- a/src/material/select/BUILD.bazel +++ b/src/material/select/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "select.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/sidenav/BUILD.bazel b/src/material/sidenav/BUILD.bazel index 12b246015cc9..7d12c7587bca 100644 --- a/src/material/sidenav/BUILD.bazel +++ b/src/material/sidenav/BUILD.bazel @@ -52,6 +52,7 @@ sass_library( sass_binary( name = "css", src = "drawer.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/slide-toggle/BUILD.bazel b/src/material/slide-toggle/BUILD.bazel index 670051f1243c..8a762d192ba3 100644 --- a/src/material/slide-toggle/BUILD.bazel +++ b/src/material/slide-toggle/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "slide-toggle.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/slider/BUILD.bazel b/src/material/slider/BUILD.bazel index e8cefdaac781..67b8cfbb0ee2 100644 --- a/src/material/slider/BUILD.bazel +++ b/src/material/slider/BUILD.bazel @@ -55,6 +55,7 @@ sass_library( sass_binary( name = "slider-css", src = "slider.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -66,6 +67,7 @@ sass_binary( sass_binary( name = "slider-thumb-css", src = "slider-thumb.scss", + layer = "angular-material", ) ng_project( diff --git a/src/material/slider/tailwind-layer.e2e.spec.ts b/src/material/slider/tailwind-layer.e2e.spec.ts new file mode 100644 index 000000000000..0c7b64c2231c --- /dev/null +++ b/src/material/slider/tailwind-layer.e2e.spec.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {browser, by, element} from 'protractor'; + +describe('Tailwind layer + Angular Material E2E', () => { + beforeEach(async () => await browser.get('/tailwind-layer')); + + it('allows utility layer styles to override Angular Material styles', async () => { + const button = element(by.id('tailwind-utility-button')); + const backgroundColor = await browser.executeScript( + 'return getComputedStyle(arguments[0]).backgroundColor;', + button.getWebElement(), + ); + + // Tailwind's lime-500 utility value. + expect(backgroundColor).toBe('rgb(132, 204, 22)'); + }); + + it('also allows unlayered utility styles to override layered Material styles', async () => { + const button = element(by.id('unlayered-utility-button')); + const backgroundColor = await browser.executeScript( + 'return getComputedStyle(arguments[0]).backgroundColor;', + button.getWebElement(), + ); + + // Unlayered author styles outrank named layers in Cascade 5. + expect(backgroundColor).toBe('rgb(217, 70, 239)'); + }); +}); diff --git a/src/material/snack-bar/BUILD.bazel b/src/material/snack-bar/BUILD.bazel index 9908896fdc62..87307ff005cd 100644 --- a/src/material/snack-bar/BUILD.bazel +++ b/src/material/snack-bar/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "container_css", src = "snack-bar-container.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", @@ -64,6 +65,7 @@ sass_binary( sass_binary( name = "css", src = "simple-snack-bar.scss", + layer = "angular-material", ) ng_project( diff --git a/src/material/sort/BUILD.bazel b/src/material/sort/BUILD.bazel index f7508bf87e58..e3a568b6a49a 100644 --- a/src/material/sort/BUILD.bazel +++ b/src/material/sort/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "sort-header.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/focus-indicators", diff --git a/src/material/stepper/BUILD.bazel b/src/material/stepper/BUILD.bazel index 362a7aa08934..777ea52765de 100644 --- a/src/material/stepper/BUILD.bazel +++ b/src/material/stepper/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "stepper.scss", + layer = "angular-material", deps = [ ":m2", ":variables", @@ -64,6 +65,7 @@ sass_binary( sass_binary( name = "header_css", src = "step-header.scss", + layer = "angular-material", deps = [ ":m2", ":variables", diff --git a/src/material/table/BUILD.bazel b/src/material/table/BUILD.bazel index 67b096982285..c2c2577c94ed 100644 --- a/src/material/table/BUILD.bazel +++ b/src/material/table/BUILD.bazel @@ -60,6 +60,7 @@ sass_library( sass_binary( name = "css", src = "table.scss", + layer = "angular-material", deps = [ ":flex_sass", ":m2", diff --git a/src/material/tabs/BUILD.bazel b/src/material/tabs/BUILD.bazel index 8096c7c9d398..271f84b7e565 100644 --- a/src/material/tabs/BUILD.bazel +++ b/src/material/tabs/BUILD.bazel @@ -65,6 +65,7 @@ sass_library( sass_binary( name = "tab_group_css", src = "tab-group.scss", + layer = "angular-material", deps = [ ":m2", ":sass", @@ -76,6 +77,7 @@ sass_binary( sass_binary( name = "tab_nav_bar_css", src = "tab-nav-bar/tab-nav-bar.scss", + layer = "angular-material", deps = [ ":sass", ], @@ -84,6 +86,7 @@ sass_binary( sass_binary( name = "tab_link_css", src = "tab-nav-bar/tab-link.scss", + layer = "angular-material", deps = [ ":sass", "//src/material/core/style:variables", @@ -93,6 +96,7 @@ sass_binary( sass_binary( name = "tab_header_css", src = "tab-header.scss", + layer = "angular-material", deps = [ ":sass", "//src/cdk:sass_lib", @@ -102,6 +106,7 @@ sass_binary( sass_binary( name = "tab_body_css", src = "tab-body.scss", + layer = "angular-material", deps = ["//src/material/core/style:layout_common"], ) diff --git a/src/material/timepicker/BUILD.bazel b/src/material/timepicker/BUILD.bazel index 1e8bcf2457ad..3e4a157f898d 100644 --- a/src/material/timepicker/BUILD.bazel +++ b/src/material/timepicker/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "timepicker_css", src = "timepicker.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/toolbar/BUILD.bazel b/src/material/toolbar/BUILD.bazel index d52db6a50a6b..d5340b6cb7df 100644 --- a/src/material/toolbar/BUILD.bazel +++ b/src/material/toolbar/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "toolbar.scss", + layer = "angular-material", deps = [ ":m2", "//src/cdk:sass_lib", diff --git a/src/material/tooltip/BUILD.bazel b/src/material/tooltip/BUILD.bazel index 5ff2539e1993..de4daf8af6b9 100644 --- a/src/material/tooltip/BUILD.bazel +++ b/src/material/tooltip/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "tooltip.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/src/material/tree/BUILD.bazel b/src/material/tree/BUILD.bazel index d727de28c820..b599d88379fa 100644 --- a/src/material/tree/BUILD.bazel +++ b/src/material/tree/BUILD.bazel @@ -53,6 +53,7 @@ sass_library( sass_binary( name = "css", src = "tree.scss", + layer = "angular-material", deps = [ ":m2", "//src/material/core/tokens:token_utils", diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 232d63d84443..87215a851ff3 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -29,17 +29,61 @@ spec_bundle = _spec_bundle http_server = _http_server ng_examples_db = _ng_examples_db -def sass_binary(sourcemap = False, include_paths = [], **kwargs): - _sass_binary( - sourcemap = sourcemap, - include_paths = include_paths, - module_mappings = { - "@angular/cdk": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/cdk", - "@angular/material": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material", - "@angular/material-experimental": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material-experimental", - }, - **kwargs - ) +def sass_binary(sourcemap = False, include_paths = [], layer = None, **kwargs): + _module_mappings = { + "@angular/cdk": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/cdk", + "@angular/material": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material", + "@angular/material-experimental": "/".join([".."] * (native.package_name().count("/") + 1)) + "/src/material-experimental", + } + + if layer: + if sourcemap: + fail("sass_binary(layer=...) does not support sourcemap because wrapping the CSS in @layer invalidates the generated mappings.") + if kwargs.get("output_name") or kwargs.get("output_dir"): + fail("sass_binary(layer=...) does not support output_name or output_dir.") + + name = kwargs.pop("name") + visibility = kwargs.pop("visibility", None) + testonly = kwargs.get("testonly", False) + src = kwargs.get("src", "") + out_css = src.rsplit(".", 1)[0] + ".css" + + # Prefix only the filename to avoid creating spurious directories. + if "/" in out_css: + out_dir, out_file = out_css.rsplit("/", 1) + raw_css = out_dir + "/_layer_" + out_file + else: + raw_css = "_layer_" + out_css + + # Compile to an intermediate CSS file, then wrap the compiled output in a named @layer + # while preserving the final filename expected by component `styleUrl`s. + _sass_binary( + name = name + "_pre_layer", + sourcemap = False, + include_paths = include_paths, + output_name = raw_css, + module_mappings = _module_mappings, + visibility = ["//visibility:private"], + **kwargs + ) + + native.genrule( + name = name, + srcs = [":" + name + "_pre_layer"], + outs = [out_css], + cmd = ("printf '@layer %s {\\n' > $@ " + + "&& cat $< >> $@ " + + "&& printf '\\n}\\n' >> $@") % layer, + testonly = testonly, + visibility = visibility, + ) + else: + _sass_binary( + sourcemap = sourcemap, + include_paths = include_paths, + module_mappings = _module_mappings, + **kwargs + ) def sass_library(**kwargs): _sass_library(**kwargs)