---
title: "Integrating Third-Party Scripts and CSS into Your Angular App"
date: "2022-10-15"
slug: "integrating-third-party-scripts-and-css-into-your-angular-app"
author: "Dany Paredes"
canonical: "https://danywalls.com/integrating-third-party-scripts-and-css-into-your-angular-app"
description: "Learn how to properly integrate third-party JavaScript libraries, CSS stylesheets, and Web Components into Angular using angular.json bundles, dynamic script loaders, and SSR-safe afterNextRender."
---


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 `afterNextRender` and Standalone Schemas](#modern-angular-dynamic-scripts-and-ssr-safety-with-afternextrender) 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:

```json
{
  "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`:

```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 to `false` if 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

```typescript
// 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

```typescript
// 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:

```typescript
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:
1. Use **`angular.json`** for global build-time stylesheets and scripts.
2. Use **`inject(DOCUMENT)`** and **`afterNextRender()`** for client-only dynamic script injection (avoiding SSR breakage).
3. Use **`CUSTOM_ELEMENTS_SCHEMA`** whenever 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](/learn-route-parameters-in-angular-with-example) and [How to Use ng-template, ng-container, and ng-content](/how-to-get-and-use-ng-template-ng-container-and-ng-content)!

