From 6ebe94c26a9d71b4d3b951f12c7fd65749804411 Mon Sep 17 00:00:00 2001 From: Francesco Manicardi Date: Tue, 11 Aug 2026 12:13:25 +0200 Subject: [PATCH] fix(material/core): elevation classes emit invalid shadows with system theme `mat.elevation-classes()` resolves each `mat-elevation-z*` class to `var(--mat-app-elevation-shadow-level-, )`, where the fallback is built from `--mat-sys-shadow`. Since that shadow color is a CSS variable name rather than a color, it was interpolated into the shadow verbatim, producing `0px 3px 5px -1px --mat-sys-shadow, ...`. That value is invalid CSS, so the browser drops the entire declaration. Apps that theme with `mat.theme` never define the `--mat-app-elevation-shadow-level-*` tokens (only the older `mat.core-theme`/prebuilt theming path does), which means they always hit the broken fallback and get no shadow at all from any `mat-elevation-z*` class. Wrap the variable in `var()` and apply the shadow opacities through `color-mix`, mirroring what `m3-utils.color-with-opacity` already does elsewhere. Themes that define the app elevation tokens are unaffected, since the fallback is unused there. --- src/material/core/style/BUILD.bazel | 1 + src/material/core/style/_elevation.scss | 17 ++++++++++--- .../core/theming/tests/m3-theme.spec.ts | 25 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/material/core/style/BUILD.bazel b/src/material/core/style/BUILD.bazel index 959dc5a7c22f..b861bec22eef 100644 --- a/src/material/core/style/BUILD.bazel +++ b/src/material/core/style/BUILD.bazel @@ -19,6 +19,7 @@ sass_library( deps = [ ":sass_utils", ":variables", + "//src/material/core/tokens:m3_utils", ], ) diff --git a/src/material/core/style/_elevation.scss b/src/material/core/style/_elevation.scss index 5a3d8c98b0ea..bf934a6ea7d7 100644 --- a/src/material/core/style/_elevation.scss +++ b/src/material/core/style/_elevation.scss @@ -4,6 +4,7 @@ @use 'sass:string'; @use './variables'; @use './sass-utils'; +@use '../tokens/m3-utils'; $_umbra-opacity: 0.2; $_penumbra-opacity: 0.14; @@ -199,10 +200,20 @@ $prefix: 'mat-elevation-z'; } @function _compute-color-opacity($color, $opacity) { - @if meta.type-of($color) == color and $opacity != null { + @if $opacity == null { + @return $color; + } + + @if meta.type-of($color) == color { @return rgba($color, $opacity); } - @else { - @return $color; + + // The color can also be the name of a CSS variable (e.g. `--mat-sys-shadow`) whose value isn't + // known at build time. Wrap it in `var()` and apply the opacity using `color-mix`, otherwise + // we'd interpolate the bare variable name into the shadow and produce invalid CSS. + @if sass-utils.is-css-var-name($color) { + @return m3-utils.color-with-opacity($color, $opacity); } + + @return $color; } diff --git a/src/material/core/theming/tests/m3-theme.spec.ts b/src/material/core/theming/tests/m3-theme.spec.ts index eaa524c78f85..b1e2d53e1a74 100644 --- a/src/material/core/theming/tests/m3-theme.spec.ts +++ b/src/material/core/theming/tests/m3-theme.spec.ts @@ -91,6 +91,31 @@ describe('M3 theme', () => { expect(nonVarProps).toEqual([]); }); + it('should emit valid elevation classes when used with the system-level theme', () => { + const root = parse( + transpile(` + html { + @include mat.theme(( + color: mat.$violet-palette, + typography: Roboto, + density: 0, + )); + } + @include mat.elevation-classes(); + `), + ); + const invalidValues: string[] = []; + root.walkDecls('box-shadow', decl => { + // Any reference to a token has to be wrapped in `var()`. A bare token name (e.g. + // `0px 2px 1px -1px --mat-sys-shadow`) is invalid CSS which makes the browser drop the + // entire declaration, resulting in no shadow at all. + if (/(^|[\s,(])--/.test(decl.value.replace(/var\(\s*--/g, 'var('))) { + invalidValues.push(decl.value); + } + }); + expect(invalidValues).toEqual([]); + }); + it('should not have overlapping tokens between theme dimensions', () => { const css = transpile(` $theme: mat.define-theme();