Reader Stacks

How to Dynamically Insert Script and Style Tags in Angular

Angular's Renderer2 service is the safe, recommended way to add a script or style tag to the DOM programmatically, rather than manipulating document directly.

Loading a third-party script (an analytics tag, a payment widget's SDK) or injecting a style block dynamically is occasionally necessary in an Angular app — Renderer2 is the framework-recommended way to do this safely, rather than reaching for document directly.

Dynamically inserting a script tag

import { Renderer2, RendererFactory2, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

export class ScriptLoaderService {
    private renderer: Renderer2;

    constructor(
        rendererFactory: RendererFactory2,
        @Inject(DOCUMENT) private document: Document
    ) {
        this.renderer = rendererFactory.createRenderer(null, null);
    }

    loadScript(src: string): Promise {
        return new Promise((resolve, reject) => {
            const script = this.renderer.createElement('script');
            script.type = 'text/javascript';
            script.src = src;
            script.onload = () => resolve();
            script.onerror = () => reject(new Error(`Failed to load script: ${src}`));

            this.renderer.appendChild(this.document.body, script);
        });
    }
}
this.scriptLoader.loadScript('https://js.stripe.com/v3/').then(() => {
    // Stripe.js is now loaded and available on window.Stripe
});

Using Renderer2 rather than directly manipulating document.body keeps the code compatible with server-side rendering (Angular Universal) and other non-browser rendering targets, where a direct document reference wouldn't work the same way.

Why the DOCUMENT injection token, not a raw document reference

Injecting DOCUMENT (Angular's platform-agnostic token) instead of referencing the global document object directly is what allows this same service to work correctly in a server-side rendering context, where Angular provides a different document implementation behind the same token.

Dynamically inserting a style tag

injectStyle(css: string): void {
    const style = this.renderer.createElement('style');
    const text = this.renderer.createText(css);

    this.renderer.appendChild(style, text);
    this.renderer.appendChild(this.document.head, style);
}
this.scriptLoader.injectStyle('.highlight { background: yellow; }');

Loading a script only once, even if the service method is called multiple times

private loadedScripts = new Set();

loadScript(src: string): Promise {
    if (this.loadedScripts.has(src)) {
        return Promise.resolve();
    }

    return new Promise((resolve, reject) => {
        const script = this.renderer.createElement('script');
        script.src = src;
        script.onload = () => {
            this.loadedScripts.add(src);
            resolve();
        };
        script.onerror = reject;
        this.renderer.appendChild(this.document.body, script);
    });
}

Tracking already-loaded script URLs prevents accidentally injecting the same third-party script multiple times across different components that each independently call the loader — genuinely useful for a widely-shared service like a payment SDK that multiple unrelated pages might each try to load.

Removing a dynamically added script or style when no longer needed

removeElement(element: any): void {
    this.renderer.removeChild(this.document.body, element);
}

Keeping a reference to the created element (returned from createElement) is necessary if it needs to be removed later — for a component-scoped script that should only be active while that specific component is displayed, cleaning it up in ngOnDestroy avoids it lingering on the page after navigation.