Integrating Third-Party Scripts and CSS into Your Angular App

When building real-world applications in Angular, you frequently need to integrate third-party libraries: analytics tracking scripts (Google Analytics, Mixpanel), payment gateways (Stripe, PayPal SDKs), charting libraries, or existing CSS frameworks.
While you could manually paste <script> tags into index.html, doing so bypasses the Angular build pipeline, compromises bundle optimization, and often causes Server-Side Rendering (SSR) crashes.
⚡ Using Modern Angular (v17+)? Jump directly to Modern Angular: SSR Safety with
afterNextRenderand Standalone Schemas to see how to load scripts safely in hydration-enabled apps.
In this guide, we will explore how to configure third-party assets in angular.json, create separate named bundles, and dynamically load external scripts safely in modern Angular.
Let's begin by looking at the built-in angular.json pipeline.
1. Build-Time Integration with angular.json 📦
The cleanest way to include global CSS and JavaScript files that must be available application-wide is through the workspace configuration file angular.json.
Navigate to architect > build > options and locate the styles and scripts arrays:
{
"architect": {
"build": {
"options": {
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
"node_modules/chart.js/dist/chart.umd.js"
]
}
}
}
}When you run ng build, the Angular CLI automatically minifies, tree-shakes (where applicable), hashes, and bundles these files into your initial distribution.
Now, what if you have a heavy script that is only used on a specific page? Let's see how to create separate named bundles.
2. Separate Named Bundles for Heavy Scripts 🗂️
If you have a large library (like a 3D canvas viewer or specialized uploader) that you do not want to bloat your initial main.js bundle with, you can configure it as a separate chunk in angular.json:
{
"styles": [
"src/styles.css",
{
"input": "src/assets/custom-uploader/uploader.css",
"inject": true,
"bundleName": "custom-uploader-styles"
}
],
"scripts": [
{
"input": "src/assets/custom-uploader/uploader.js",
"inject": false,
"bundleName": "custom-uploader-script"
}
]
}Options Explained:
input: Path to the source file (relative to workspace root).bundleName: Name of the generated output file (e.g.dist/my-app/custom-uploader-script.js).inject: Set tofalseif you want to load the script on-demand via JavaScript instead of including it on initial page load.
Now let's see how to dynamically load scripts at runtime in a component.
3. Modern Angular: Dynamic Scripts & SSR Safety with afterNextRender ⚡
With Angular's modern hydration and Server-Side Rendering (SSR) enabled by default, attempting to access document or window directly during component initialization will crash the server renderer with ReferenceError: document is not defined.
In modern Angular (v17+), we use the afterNextRender lifecycle hook to guarantee that script injection only runs once the component has rendered on the client browser.
Reusable Dynamic Script Service
// script-loader.service.ts
import { Injectable, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class ScriptLoaderService {
private readonly document = inject(DOCUMENT);
private readonly loadedScripts = new Set<string>();
loadScript(src: string): Promise<void> {
if (this.loadedScripts.has(src)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = this.document.createElement('script');
script.type = 'text/javascript';
script.src = src;
script.async = true;
script.onload = () => {
this.loadedScripts.add(src);
resolve();
};
script.onerror = (err) => reject(err);
this.document.head.appendChild(script);
});
}
}Safe Usage Inside a Standalone Component
// payment-checkout.component.ts
import { Component, inject, afterNextRender, signal } from '@angular/core';
import { ScriptLoaderService } from './script-loader.service';
@Component({
selector: 'app-payment-checkout',
standalone: true,
template: `
<div class="checkout-card p-6 border rounded-xl">
<h2>Complete Your Purchase</h2>
@if (scriptReady()) {
<div id="paypal-button-container"></div>
} @else {
<p class="text-neutral-500">Loading payment gateway securely...</p>
}
</div>
`
})
export class PaymentCheckoutComponent {
private readonly scriptLoader = inject(ScriptLoaderService);
readonly scriptReady = signal(false);
constructor() {
// Guaranteed to only execute in the browser DOM (SSR Safe!)
afterNextRender(async () => {
try {
await this.scriptLoader.loadScript('https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID');
this.scriptReady.set(true);
} catch (err) {
console.error('Failed to load payment SDK:', err);
}
});
}
}Now let's look at integrating Web Components / Custom Elements.
4. Using Third-Party Web Components with CUSTOM_ELEMENTS_SCHEMA 🧩
If your third-party script defines a standard Web Component (e.g. <stripe-pricing-table> or <media-player>), Angular's template compiler will emit an error by default because it doesn't recognize the custom HTML tag.
To allow custom elements in modern Standalone components:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-custom-widget',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA], // 👈 Tells Angular to allow unknown custom tags
template: `
<stripe-pricing-table
pricing-table-id="prctbl_12345"
publishable-key="pk_live_12345"
></stripe-pricing-table>
`
})
export class CustomWidgetComponent {}Integration Strategy Comparison 📊
| Integration Method | Build-Time or Runtime? | SSR Safe? | Best Use Case |
|---|---|---|---|
angular.json (styles/scripts) | Build-time | ✅ Yes | Global CSS frameworks, shared utility scripts |
angular.json Named Bundles | Build-time | ✅ Yes | Large secondary modules loaded conditionally |
afterNextRender + Dynamic <script> | Runtime (Client) | ✅ Yes | Analytics, payment SDKs (Stripe/PayPal), ad trackers |
CUSTOM_ELEMENTS_SCHEMA | Template Compile | ✅ Yes | Third-party Web Components and Microfrontends |
Recap 🛠️
To keep your Angular application fast, maintainable, and SSR-safe when adding third-party code:
- Use
angular.jsonfor global build-time stylesheets and scripts. - Use
inject(DOCUMENT)andafterNextRender()for client-only dynamic script injection (avoiding SSR breakage). - Use
CUSTOM_ELEMENTS_SCHEMAwhenever embedding custom Web Component tags in your templates.
For more modern Angular architectural guides, check out my articles on How to Use Route Parameters in Angular and How to Use ng-template, ng-container, and ng-content!
These are my experiences learning and facing my daily challenges working with Angular.
Frequently Asked Questions
What is the best way to add global third-party CSS or JS scripts in Angular?
The standard and most maintainable approach is configuring the 'styles' and 'scripts' arrays inside your angular.json file under 'architect > build > options'. The Angular CLI compiles, optimizes, hashes, and injects them automatically into your output bundles.
How do I load a third-party script dynamically on demand in a specific component?
You can inject the DOCUMENT token and create a <script> element dynamically inside the afterNextRender() lifecycle hook. This ensures the script only executes in the browser and prevents server-side rendering (SSR) crashes.
How do I use third-party Web Components / Custom Elements in Angular?
To use custom elements without Angular throwing unknown element errors, add 'CUSTOM_ELEMENTS_SCHEMA' to the schemas array in your Standalone component or NgModule declaration.
How do I create a separate named bundle for a heavy third-party script in angular.json?
In angular.json, specify the script as an object with 'input', 'bundleName', and 'inject: false' (or true). Angular CLI will generate a dedicated output chunk that you can lazy load when needed.
Related Articles
How to Use ng-template, ng-container, and ng-content in Angular (Complete Guide)
Learn the exact differences between ng-template, ng-container, and ng-content in Angular, how to pass context with ngTemplateOutlet, multi-slot projection, and how they evolve with modern @if, @for, and @defer control flow.
How to Use Route and Query Parameters in Angular (with Signals & Inputs)
Learn how to pass and read route parameters, query parameters, and matrix parameters in Angular using Signals, withComponentInputBinding, and ActivatedRoute.
How to Share Data Between Components in Angular (Signals & Services)
Learn the best patterns to share data between Angular components. Compare legacy Input/Output decorators and BehaviorSubjects with modern Signals, input(), output(), model(), and Signal-based services.
Share this article
If you found this guide helpful, consider sharing it with your team or fellow developers.
Real Software. Real Lessons.
I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.