Skip to content
Next Next commit
refactor(core): add StyleRoot concept
This represents a unique location where styles can be rendered and will apply to distinct elements. This is defined as simply `Document | ShadowRoot`. Styles rendered to a `Document` should go in its `HTMLHeadElement` while styles applied to a `ShadowRoot` should just be rendered within that root.

This needs to be public API as it will be added to `Renderer2` which is already public API.
  • Loading branch information
dgp1130 committed Jan 20, 2026
commit ea5d430ee5da34b27564d10f1b8bafeb68835464
3 changes: 3 additions & 0 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,9 @@ export interface StreamingResourceOptions<T, R> extends BaseResourceOptions<T, R
stream: ResourceStreamingLoader<T, R>;
}

// @public
export type StyleRoot = Document | ShadowRoot;

// @public
export class TemplateRef<C> {
createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef<C>;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
*/

// Public API for render
export {Renderer2, RendererFactory2, ListenerOptions} from './render/api';
export {Renderer2, RendererFactory2, ListenerOptions, StyleRoot} from './render/api';
export {RendererStyleFlags2, RendererType2} from './render/api_flags';
29 changes: 29 additions & 0 deletions packages/core/src/render/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@ import {getComponentLViewByIndex} from '../render3/util/view_utils';

import {RendererStyleFlags2, RendererType2} from './api_flags';

/**
* A location where CSS stylesheets may be added.
*
* @publicApi
*/
export type StyleRoot = Document | ShadowRoot;

/**
* Asserts that the given node is a {@link StyleRoot}. Useful for converting the return value of
* {@link Node.prototype.getRootNode} into a {@link StyleRoot}. If the root is a detached node,
* this returns `undefined` as there is no meaningful style root to attach styles to.
*
* @param root The root node of a DOM tree to convert to a {@link StyleRoot}.
* @returns The given root as a {@link StyleRoot} if it is a valid root for styles. Otherwise
* returns `undefined`.
*/
export function asStyleRoot(root: Node): StyleRoot | undefined {
// Need to feature-detect `ShadowRoot` for Node environments where DOM emulation does not
// support it.
if (
root instanceof Document ||
(typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot)
) {
return root;
}

return undefined;
}

/**
* Creates and initializes a custom renderer that implements the `Renderer2` base class.
*
Expand Down
34 changes: 34 additions & 0 deletions packages/core/test/render/api_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @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 {isNode} from '@angular/private/testing';
import {asStyleRoot} from '../../src/render/api';

describe('api', () => {
describe('asStyleRoot', () => {
it('returns an input `Document`', () => {
expect(asStyleRoot(document)).toBe(document);
});

it('returns an input `ShadowRoot`', () => {
// Shadow DOM isn't implemented in DOM emulation.
if (isNode) {
expect().nothing();
return;
}

const shadowRoot = document.createElement('div').attachShadow({mode: 'open'});

expect(asStyleRoot(shadowRoot)).toBe(shadowRoot);
});

it('returns `undefined` for a detached node', () => {
expect(asStyleRoot(document.createElement('div'))).toBeUndefined();
});
});
});