Skip to content

provideCssVarNamespacing() breaks styling of third-party libraries themed by global CSS variables #70064

Description

@lacolaco

Which @angular/* package(s) are the source of the bug?

compiler

Is this a regression?

No

Description

CSS Variables namespacing, introduced in v22.1.0, rewrites custom property references inside a component's styles unconditionally, while global stylesheets are left alone by design. For a library published with partial compilation that rewrite does not happen when the library is built. It happens during the consuming application's build, with the consumer's namespace.

As a result, calling provideCssVarNamespacing() breaks any third-party library that exposes theming through custom properties defined in a global stylesheet, which is the usual way libraries do it. Angular Material ends up completely unstyled.

With provideCssVarNamespacing():

with provideCssVarNamespacing

Without:

without provideCssVarNamespacing

With @angular/material and the prebuilt azure-blue theme, a plain <button matButton="filled">:

computed with provideCssVarNamespacing() without
background-color rgba(0, 0, 0, 0) rgb(0, 92, 187)
color rgb(0, 0, 0) rgb(255, 255, 255)
border-radius 0px 9999px

The theme defines its tokens in the global stylesheet and the components reference them, so the two sides end up under different names:

$ grep -o -- "--mat-sys-[a-z-]*:" node_modules/@angular/material/prebuilt-themes/azure-blue.css | sort -u | wc -l
157

$ grep -o -- "var(--mat-" node_modules/@angular/material/fesm2022/button.mjs | wc -l
832
getComputedStyle(document.documentElement).getPropertyValue('--mat-sys-primary')       // "#005cbb"
getComputedStyle(document.documentElement).getPropertyValue('--myapp_mat-sys-primary') // ""

This is not specific to Material. A minimal library component with color: var(--brand) is published as

// dist/ui/fesm2022/ui.mjs
styles: ["#lib{color:var(--brand);border:2px solid var(--brand);...}"]

and comes out of the consuming application's build as

#lib[_ngcontent-%COMP%]{color:var(--%NS%brand)}

because PartialComponentLinkerVersion1.linkPartialDeclaration re-runs compileComponentFromMetadata, which applies namespaceCssVariables to styles unconditionally (packages/compiler/src/render3/view/compiler.ts).

--global-- is the designed opt-out, but it only helps in source you control. A library would have to spell every one of its tokens --global--… to defend itself against a decision its consumers make, and libraries published before v22.1 obviously cannot. A consuming application has no way to apply the opt-out to a dependency.

Steps to reproduce:

  1. ng new an application on 22.1
  2. npm i @angular/material @angular/cdk and add node_modules/@angular/material/prebuilt-themes/azure-blue.css to the styles array in angular.json
  3. put <button matButton="filled">Filled button</button> in the template
  4. add provideCssVarNamespacing() to appConfig.providers
  5. build and open the page

Please provide a link to a minimal reproduction of the bug

https://github.com/lacolaco/angular-cssvar-namespacing-library-repro

Please provide the exception or error you saw

No errors. The declarations are silently dropped.

Please provide the environment you discovered this bug in (run ng version)

Angular CLI       : 22.1.2
Angular           : 22.1.0
Node.js           : 22.22.3
Package Manager   : npm 10.9.8
Operating System  : darwin arm64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package                   │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build            │ 22.1.2            │ ^22.1.2           │
│ @angular/cdk              │ 22.1.0            │ ^22.1.0           │
│ @angular/cli              │ 22.1.2            │ ^22.1.2           │
│ @angular/common           │ 22.1.0            │ ^22.1.0           │
│ @angular/compiler         │ 22.1.0            │ ^22.1.0           │
│ @angular/compiler-cli     │ 22.1.0            │ ^22.1.0           │
│ @angular/core             │ 22.1.0            │ ^22.1.0           │
│ @angular/forms            │ 22.1.0            │ ^22.1.0           │
│ @angular/material         │ 22.1.0            │ ^22.1.0           │
│ @angular/platform-browser │ 22.1.0            │ ^22.1.0           │
│ @angular/router           │ 22.1.0            │ ^22.1.0           │
│ ng-packagr                │ 22.1.0            │ ^22.1.0           │
│ rxjs                      │ 7.8.2             │ ~7.8.0            │
│ typescript                │ 6.0.3             │ ~6.0.2            │
└───────────────────────────┴───────────────────┴───────────────────┘

Anything else?

For context on prior art, in case this reads as already known: the closest discussion I found is #67362, where the mismatch between a var() reference and a declaration living in a global stylesheet was raised, and --global-- was chosen as the answer.

What doesn't appear in either PR is the distribution dimension. Neither discussion mentions partial compilation, the linker, or npm packages, and the single sentence about libraries in the description is about using CssVarNamespacer for JS references rather than a library's own styles. --global-- only helps in source the author controls, which is what makes this case different from an application namespacing itself. I can't see the internal design doc linked from #67362, so this may well have been considered there.

If the answer here is that libraries need to adopt --global-- for their tokens, then adopting this feature is gated on an ecosystem-wide source change rather than on an application-level provider. Every library published before 22.1 is incompatible with any consumer that turns the flag on, there is no version constraint or diagnostic that surfaces it, and the failure mode is styles silently not applying. That seems like a larger coordination problem than a minor release can carry. --global- with a single hyphen is already slated to become an error in v23, so parts of this are on a major track anyway.

It also seems worth asking whether linked library styles should be namespaced at all. The namespace is application-scoped (it defaults to APP_ID), so rewriting a dependency's styles during the consuming application's link step is a choice rather than a consequence. Leaving them alone would still give the feature what it is after, an application's own variables no longer colliding with another application's on the same page, while keeping libraries working.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: compilerIssues related to `ngc`, Angular's template compilerfeatureLabel used to distinguish feature request from other issues

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions