Just to make things clear, Salesforce provides an excellent guides titled Use Third-Party JavaScript Libraries and Use Third-Party Web Components in LWC, which covers everything thoroughly. In 80% of cases, this information is enough to get started, but if you’re in those 20% who can’t get it to work — let’s dig in, my fellow Salesforce Developers!

The information provided was compiled during the Lightning Web Development workshop and offers detailed guidance on how to get third-party JavaScript code working with your Salesforce org.


Types of Third-Party JavaScript Code

To begin, let’s distinguish clearly between the two types of JavaScript code that you can use with your Lightning Web Components.

Third-Party Web Components

This is a JavaScript file that defines a class extending the native HTMLElement class. This class is responsible for creating a custom HTML element and provides a visible User Interface (UI) element.

It could be something familiar, like a datatable, developed outside of Salesforce. You upload it as a static resource and use it within your LWC. This same datatable might include features like lazy-loading and corresponding data handling. See an example below:

class MyCustomElement extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

    // Create a shadow root
    this.shadowRoot = this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    // Element is added
    this.shadowRoot.innerHTML = "<div></div>";
  }
  
  // ...
}

// Registers the element
customElements.define("my-custom-element", MyCustomElement);

Third-Party JavaScript Libraries

This is a JavaScript file that provides reusable functions or objects. It’s not designed to represent visual elements directly, like HTML tags. Instead, it’s more like a “utils.js”, you call its functions when you need them. Some libraries may create visual output, like charts or animations, but they do so programmatically and don’t define custom HTML elements. For instance:

(function (global) {
    function Chart(ctx, config) {
        // Very simplified chart renderer
        ctx.fillStyle = config.data.datasets[0].backgroundColor[0];
        ctx.fillRect(10, 10, 100, 100);
    }

    global.Chart = Chart;
})(window);

Why Isn’t My Third-Party Web Component Working?

Lightning Web Security (LWS) must be enabled in the Salesforce org because Lightning Locker doesn’t support third-party web components.

Let’s say you found the perfect component on webcomponents.org, added it to your static resources, imported it into your Lightning Web Component using loadScript, and used the lwc:external attribute in your markup to indicate that it’s an external web component. And yet, it doesn’t work as expected.

import { LightningElement } from 'lwc';
import yourstaticresource from '@salesforce/resourceUrl/webcomponent';
import { loadScript } from 'lightning/platformResourceLoader';

export default class MyComponent extends LightningElement {
    componentLoaded = false;

    renderedCallback() {
        if (!this.componentLoaded) {
          this.componentLoaded = true;
          loadScript(this, yourstaticresource)
            .then(() => {
              console.log('Custom web component loaded');
            })
            .catch(error => {
              console.error('Failed to load web component:', error);
            });
        }
    }
}
<template>
    <your-web-component lwc:external></your-web-component>
</template>

In most cases, you’ll need to modify the code of the web component itself to make it work. So, let’s look at the most common problems:

Open Shadow DOM

You’ve probably heard that one of the “favorite things” among Salesforce Developers is the Shadow DOM. This Shadow DOM can operate in two modes: open and closed.

Now, here’s the crucial part, your 3rd-party web component CANNOT use the open Shadow DOM, and if it does — expect it to behave incorrectly in Salesforce.

But what does that actually mean?

An open Shadow DOM allows external sources to access the component’s shadow tree via the shadowRoot property on the element. In contrast, a closed Shadow DOM prevents such direct access, keeping the internal structure fully encapsulated.

Salesforce enforces the use of the closed shadow mode. To ensure compatibility, open the source code of your Web Component and locate the following line:

class MyCustomElement extends HTMLElement {
  constructor() {
    // ...

    // Create an open shadow root
    this.shadowRoot = this.attachShadow({ mode: "open" });
  }

// ...

Your task is to replace the mode: "open” attribute with mode: "closed”

Next, identify all occurrences of the shadowRoot property in the code, including the initial assignment line, and replace them with _shadow (or any other custom name, but not shadowRoot) as demonstrated below:

❌ this.shadowRoot = this.attachShadow({ mode: 'open' });
❌ this.shadowRoot.innerHTML = `<p>Hello from shadow DOM</p>`;

✅ this._shadow = this.attachShadow({ mode: 'closed' });
✅ this._shadow.innerHTML = `<p>Hello from CLOSED shadow DOM</p>`;

Global Access Is Restricted

If a 3rd-party web component uses the global document object to retrieve an existing element by ID (note: creating a new element is acceptable), this can and will lead to unexpected behavior.

To resolve this issue, open the source code of the web component and locate all instances where an element is queried by ID. For example:

document.getElementById('some-id');

There are many possible approaches here, but one of the simplest, and my preferred approach, is to use the component’s Shadow DOM instead of accessing the global document object. For instance:

class MyCustomElement extends HTMLElement {
  constructor() {
      this._shadow = this.attachShadow({ mode: 'closed' });
  }

  someFunction() {
      ❌ document.getElementById('some-id');
      ✅ this._shadow.getElementById('some-id');
  }

// ...

Dependencies or Modules

If you’ve reached this point and your web component still doesn’t work, the most likely issue is that the component uses or relies on external dependencies or ES modules. What does this mean?

For example, suppose your Web Component includes the following line of code:

<script type="module" src="https://cdn.example.com/button-component.js"></script>

What’s actually happening here is that the Web Component is attempting to load a JavaScript module directly from an external source, a resource that hasn’t been uploaded as a static resource in your Salesforce org. The rule is simple: only scripts that have been added as static resources and loaded using loadScript() can be used in Lightning Web Components (LWC). Anything else, such as external <script type="module"> tags, won't work.

A similar issue occurs when trying to use Node.js packages or modules that have been imported directly into a web component. While this is not a common use case, since web components are generally expected to be self-contained, it still happens. For instance:

import someLibrary from 'some-npm-lib';

So, what’s the solution?

Admittedly, this can be a bit tricky — but never say never.

These libraries often consist of multiple interdependent JavaScript files, so the only solution is to bundle them into a single JS file. In other words, analyze the dependencies and references, then compile everything into one standalone JavaScript file.

You can do this manually or use dedicated tools like Browserify or Webpack, and I highly recommend the latter.

Since the process can be a bit time-consuming, I’ve prepared a detailed tutorial walking you through every step, check it out.

In the meantime, here are the basic commands you can use with Browserify:

# Note: Chart.js was taken as an example only

# 1. Install Browserify as a local development dependency
npm install browserify

# 2. This is just a path to Chart.js’s package.json file — to find the main .js file
node_modules/chart.js/package.json

# 3. Bundle the main file of Chart.js module into a single browser-compatible JavaScript file
npx browserify node_modules/chart.js/dist/chart.cjs -o chart.bundled.js

# 4. Same as above, but exposes the bundled library as a global variable named "TEST" (i.e., accessible via window.TEST)
npx browserify node_modules/chart.js/dist/chart.cjs -o chart.bundled2.js -s TEST

Why Isn’t My JavaScript Library Working?

Lightning Web Security (LWS) is recommended to be enabled in Salesforce organizations because Lightning Locker has more restrictions. We will consider this case with LWS in mind.

Let’s say you found the perfect library, such as docx-merger, added it to your static resources, and imported it into your Lightning Web Component using loadScript. What's next?

On a positive note, I can say that almost all libraries should work as is, unless, of course, they have any dependencies. However, what is important to understand when using third-party libraries within the Lightning Web Security (LWS) scope is the following:

  1. Your library will never have access to the global window object.
  2. Your library will be placed in a so-called “LWS Sandbox”, a kind of isolated container where it seems to be unrestricted, but it does not get access to the global window object, only to its copy that is inside the sandbox.
  3. The functions and objects of your library will be accessible to the Lightning Web Component using the window object. And although it can work without it, it is still recommended to use it this way.

Let’s look at this example using an object and a function from our docx-merger library that we have already loaded into static resources:

import { LightningElement } from 'lwc';
import docxmerger from "@salesforce/resourceUrl/docxmerger";
import { loadScript } from "lightning/platformResourceLoader";

export default class MergeDocx extends LightningElement {
  docxLoaded = false;

  renderedCallback() {
    if (this.docxLoaded) return;

    loadScript(this, docxmerger)
      .then(() => {
        this.docxLoaded = true;
        // The library was added to the 'window' object
        console.log('DocxMerger library loaded:', window.DocxMerger);
      });
  }

  async handleMerge() {
    // ...
    const mergedBlob = await this.mergeWithDocxMerger(base64Blobs, true, true);
  }

  async mergeWithDocxMerger(documents, isBase64Conversion, isDownload) {
    const base64Documents = isBase64Conversion
      ? this.encodeBlobToBase64Format(documents)
      : documents;

      // Call the object & functions from the 'window' object
      const merger = new window.DocxMerger({}, base64Documents);
      merger.save('blob', function (data) {
        // ...
      });
  }

  encodeBlobToBase64Format(documents) {
    return documents.map(base64 => this.base64ToArrayBuffer(base64));
  }

  base64ToArrayBuffer(base64) {
    const binaryString = window.atob(base64);
    const len = binaryString.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
  }
}

In this case, we follow the documentation of the specific library almost exactly, with one small change: when calling the DocxMerger object, we specify that it is located in the window object. The standard documentation does not mention this, as it is specific to Lightning Web Security (LWS).

Fallback Mechanism

As you can see from this documentation, and as I mentioned earlier, your third-party JavaScript code does not have access to the global scope because it is sandboxed. However, your library is likely unaware of this.

If you encounter difficulties with your library, for example, if the global variable is undefined, you need to provide a fallback mechanism. To do this, open the source code of your JavaScript library and locate the assignment of the global scope, which will look something like this:

(function (global) {
  // ...
})(this);

In this code, this is passed to the closure function as global. With Lightning Web Security (LWS), this and global are not defined, so this is likely to cause issues. To fix this, you need to modify the code as follows:

(function (global) {
  global = global || self;
  global.myFunction = function () {};
})(this);

That is, we have added a new condition: if global is undefined, assign it to self, in other words, the scope of sandbox. This allows your library to function as expected while complying with security requirements.


These problems with third-party JavaScript components or libraries can sometimes arise. Do you think it’s worth spending time modifying the source code, or is it better to settle for less ideal but functional solutions?

Share your thoughts in the comments!