Skip to content

CSS Variable Namespacing#68846

Open
mattrbeck wants to merge 3 commits into
angular:mainfrom
mattrbeck:css_var_namespacing
Open

CSS Variable Namespacing#68846
mattrbeck wants to merge 3 commits into
angular:mainfrom
mattrbeck:css_var_namespacing

Conversation

@mattrbeck
Copy link
Copy Markdown
Member

@mattrbeck mattrbeck commented May 20, 2026

Superseds #67362. Primary differences are:

  • Opt-out syntax is now a --global prefix, e.g. --global--foo: blue
  • Added support for style properties, e.g. [style.--foo]="'blue'"
  • Added errors for prefix missing trailing double-hyphen, e.g. --global-foo

This adds CSS variable namespacing support to Angular.

This allows multiple apps to coexist on the same page with isolated CSS variables, meaning one can use color: var(--primary-color); without worrying about accidentally inheriting the primary color of a different app which happens to set it on an ancestor element.

To enable this feature, call provideCssVarNamespacing in your app.config.ts. Typically you want to configure this with the same value as APP_ID, but with an additional separator at the end (a - or _):

import {ApplicationConfig, APP_ID} from '@angular/core';
import {provideCssVarNamespacing} from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_ID,
      useValue: 'my-app',
    },
    provideCssVarNamespacing('my-app_'),
  ],
};

This only namespaces styles in Angular components (the styles or styleUrls properties in @Component). It does not namespace global styles, which are out of scope for this effort.

Namespacing does naturally break any JavaScript references to CSS variables, therefore this PR also introduces CssVarNamespacer which allows you to automatically namespace variables based on what is configured in the application.

import {CssVarNamespacer} from '@angular/platform-browser';

const namespacer = inject(CssVarNamespacer);
const color = namespacer.namespace('--primary-color');
getComputedStyle(someElement).getPropertyValue(color);

Libraries should consider always using the namespacer when referring to CSS variables, as they may be consumed by applications which enable namespacing.

Namespacing works by having the compiler unconditionally prepend %NS% to CSS variables (--foo -> --%NS%foo) and then at runtime replaces %NS% with a namespace specified by provideCssVarNamespacing('my-app_') (--%NS%foo -> --my-app_foo).

Internal bug: b/485672083


Closes #67362 via supersession.

@pullapprove pullapprove Bot requested review from crisbeto and kirjs May 20, 2026 23:47
@angular-robot angular-robot Bot added the detected: feature PR contains a feature commit label May 20, 2026
@mattrbeck mattrbeck force-pushed the css_var_namespacing branch from 6c27311 to 302644d Compare May 21, 2026 00:01
@angular-robot angular-robot Bot added the area: compiler Issues related to `ngc`, Angular's template compiler label May 21, 2026
@ngbot ngbot Bot added this to the Backlog milestone May 21, 2026
@mattrbeck mattrbeck requested review from dgp1130 and removed request for kirjs May 21, 2026 00:02
@pullapprove pullapprove Bot requested a review from atscott May 21, 2026 00:03
mattrbeck added 3 commits May 20, 2026 17:19
Adds logic to inject symbols into CSS variables for runtime namespacing.
The runtime now replaces instances of `%NS%` with a namespacing
variable, limiting reach of CSS variables to the current app. An opt-out
syntax of a `--global` prefix allows users to avoid this behavior.
Using `--global-foo` is now prohibited. We suspect these cases will
likely be typos of `--global--foo` in the future, so we blanket ban them
and direct users to the expected syntax.
Adds support for namespacing css variables in style properties. Behaves
as you'd expect following the implementation for stylesheets generally.

This change also moves the error message into a util function since we
now need to produce the same error in three places.
@mattrbeck mattrbeck force-pushed the css_var_namespacing branch from 302644d to e95f942 Compare May 21, 2026 00:22
* followed by a separator, such as 'my-app_'.
* @publicApi
*/
export function provideCssVarNamespacing(namespace: string): EnvironmentProviders {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for reviewer: I'm inclined to automatically add a _ suffic to the provided namespace if it's non-empty and doesn't terminate with a - or _. Any opinions? Might be nice to automatically add the separator.

@Inject(CSS_VAR_NAMESPACE) @Optional() cssVarNamespace: string | null = null,
) {
this.defaultRenderer = new DefaultDomRenderer2(eventManager, doc, ngZone, this.tracingService);
this.cssVarNamespace = cssVarNamespace ?? '';
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for reviewer: Rather than falling back to an empty string, I'm inclined to default to the APP_ID. If a user wants to disable prefixing app-wide, they could still provideCssVarNamespacing(''). Thoughts?

// Validate that the whole `--foo` variable is passed in.
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!name.startsWith('--')) {
throw new Error(
Copy link
Copy Markdown
Contributor

@SkyZeroZx SkyZeroZx May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a RuntimeError ?

*
* @publicApi
*/
@Injectable({providedIn: 'root'})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
@Injectable({providedIn: 'root'})
@Service()

*
* Typically set via {@link provideCssVarNamespacing}.
*/
export const CSS_VAR_NAMESPACE = new InjectionToken<string>('CSS_VAR_NAMESPACE');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can save a few bytes by doing

Suggested change
export const CSS_VAR_NAMESPACE = new InjectionToken<string>('CSS_VAR_NAMESPACE');
export const CSS_VAR_NAMESPACE = new InjectionToken<string>(typeof ngDevMode !== 'undefined' && ngDevMode ? 'CSS_VAR_NAMESPACE' : '');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other suggestion, what if that token had a default factory ?
This was we would skip that optional: true part everywhere it is injected

* This is useful when reading or setting CSS variables dynamically in JavaScript that
* were transformed by the compiler during the build.
*
* @publicApi
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @publicApi
* @publicApi 22.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: compiler Issues related to `ngc`, Angular's template compiler compiler: styles core: CSS encapsulation core: stylesheets detected: feature PR contains a feature commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants