Salesforce · · 17 min read

An Introduction to the DOM for LWC

Understanding the DOM for Lightning Web Components — what the DOM is, the difference between Light DOM, Shadow DOM, and Synthetic Shadow DOM, and how Lightning Locker Service and Lightning Web Security work.

Part 73: An Introduction to the DOM for LWC

Welcome back to the Salesforce series. In Part 72, we covered the basics of Lightning Web Components — the file structure, the component lifecycle, data binding, and how to get a simple component rendering on the page. Now we need to go a level deeper. Before you can build real-world LWC applications, you need to understand the environment your components live in. That environment is the DOM.

The DOM is one of those concepts that every web developer uses every day but rarely stops to think about carefully. In the context of Lightning Web Components, though, the DOM matters more than usual because Salesforce does not give you a plain, open DOM to work with. Your components run inside layers of encapsulation and security that affect how you query elements, how your styles cascade, and how your components communicate with each other. If you have ever wondered why your CSS is not applying to a child component, why document.querySelector does not return what you expect, or what Lightning Web Security actually does, this post will clear things up.

Let us start from the very beginning.


What is the DOM?

The DOM stands for Document Object Model. It is the browser’s in-memory representation of an HTML document as a tree of objects. When the browser loads an HTML file, it does not keep the raw text around for rendering. Instead, it parses the HTML and builds a tree structure where every element, attribute, and piece of text becomes a node. This tree is the DOM, and it is what JavaScript interacts with to read, modify, and respond to events on the page.

Here is a simple HTML document:

<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
  </head>
  <body>
    <div class="container">
      <h1>Hello</h1>
      <p>Welcome to my page.</p>
    </div>
  </body>
</html>

The browser parses this and creates a tree that looks conceptually like this:

document
  └── html
        ├── head
        │     └── title
        │           └── "My Page"
        └── body
              └── div.container
                    ├── h1
                    │     └── "Hello"
                    └── p
                          └── "Welcome to my page."

Every box in that tree is a node. Element nodes represent HTML tags. Text nodes represent the text content inside those tags. The document object sits at the top and serves as the entry point for all DOM operations.

Why the DOM Matters

JavaScript interacts with the page entirely through the DOM. When you write document.querySelector('.container'), you are traversing this tree to find the first element with the class container. When you write element.textContent = 'New text', you are modifying a text node in the tree. When you add an event listener, you are telling the browser to watch a specific node for interactions.

The key thing to understand is that the DOM is live. If you change a node in the tree, the browser re-renders the affected portion of the page. This is how dynamic web applications work — JavaScript modifies the DOM, and the browser reflects those changes visually.

The DOM API

The browser provides a set of methods for working with the DOM. The most common ones include:

// Finding elements
document.getElementById('myId');
document.querySelector('.myClass');
document.querySelectorAll('div');

// Creating and adding elements
const newDiv = document.createElement('div');
newDiv.textContent = 'I am new';
document.body.appendChild(newDiv);

// Modifying elements
const heading = document.querySelector('h1');
heading.style.color = 'blue';
heading.setAttribute('data-custom', 'value');

// Removing elements
const old = document.querySelector('.old');
old.remove();

In a traditional web application, you have full access to the entire DOM tree. You can query any element on the page, modify any node, and traverse up and down the tree freely. But in Lightning Web Components, things are different. Your component does not own the whole page. It owns a small subtree, and Salesforce wraps that subtree in layers of encapsulation to keep components isolated from each other. That brings us to Shadow DOM.


What is the Light DOM, Shadow DOM, and Synthetic Shadow DOM?

These three terms describe different approaches to how a component’s internal DOM structure relates to the rest of the page. Understanding the differences is essential for writing LWC code that works correctly.

Light DOM

The Light DOM is the regular, standard DOM that you are already familiar with. When you write plain HTML and the browser renders it, everything lives in the Light DOM. There is no encapsulation. Any CSS rule on the page can reach any element. Any JavaScript can query any node. Everything is globally accessible.

In a Light DOM world, if you have two components on the page and both define a .title class with different styles, they will conflict. Whichever CSS rule has higher specificity or loads last wins. This is the classic CSS specificity problem that every frontend developer has dealt with.

For most of LWC’s history, Light DOM was not the default. But Salesforce introduced Light DOM support for LWC to address scenarios where you need global styles to apply to your component’s markup — for example, when building components that must integrate with a CMS or a design system that relies on global stylesheets.

To enable Light DOM rendering for an LWC component, you set the renderMode in the component’s JavaScript file:

import { LightningElement } from 'lwc';

export default class MyComponent extends LightningElement {
    static renderMode = 'light';
}

When a component renders in Light DOM mode, its internal markup is placed directly into the regular DOM tree. There is no shadow boundary. External styles apply to the component’s elements, and the component’s styles can leak out to the rest of the page. This gives you flexibility but sacrifices isolation.

Shadow DOM

The Shadow DOM is a web standard (part of the Web Components specification) that provides DOM and style encapsulation for a component. When a component uses Shadow DOM, the browser creates a separate, hidden DOM tree — called a shadow tree — attached to the component’s host element. The internal markup of the component lives inside this shadow tree and is invisible to the outside world.

Here is a conceptual diagram of how Shadow DOM works:

document
  └── body
        └── my-component (host element)
              └── #shadow-root
                    ├── h1
                    │     └── "Component Title"
                    └── p
                          └── "Component content."

The #shadow-root is the boundary. Everything inside it is encapsulated. CSS defined outside the shadow root does not penetrate into it, and CSS defined inside the shadow root does not leak out. JavaScript running in the main document cannot use document.querySelector to find elements inside the shadow root. If you want to access elements inside the shadow tree from within the component, you use this.template.querySelector instead.

// Inside a Shadow DOM component
export default class MyComponent extends LightningElement {
    renderedCallback() {
        // This works -- queries within the component's shadow tree
        const heading = this.template.querySelector('h1');

        // document.querySelector('h1') would NOT find the h1
        // inside this component's shadow root
    }
}

This encapsulation is powerful. It means you can write CSS class names like .title or .container inside your component without worrying about conflicts with other components on the page. Each component is a self-contained unit with its own private DOM and styles.

Native Shadow DOM is the browser’s built-in implementation. It is part of the web platform and supported by all modern browsers. When Salesforce says a component runs in Shadow DOM mode, this is what they are aiming for — true browser-native encapsulation.

Synthetic Shadow DOM

For years, Salesforce did not use native Shadow DOM for LWC. Instead, they implemented their own version called Synthetic Shadow DOM. This was a polyfill — a JavaScript layer that mimicked the behavior of native Shadow DOM without actually using the browser’s native implementation.

Why? When LWC was first introduced, native Shadow DOM had limitations and inconsistencies across browsers, and Salesforce needed to support scenarios that native Shadow DOM did not handle well at the time. So they built a synthetic version that provided similar encapsulation behavior but ran entirely in JavaScript.

Synthetic Shadow DOM works like this:

document
  └── body
        └── my-component (host element)
              ├── h1 [with synthetic attributes for scoping]
              │     └── "Component Title"
              └── p [with synthetic attributes for scoping]
                    └── "Component content."

Notice that there is no real #shadow-root. The component’s elements are placed in the regular DOM, but Salesforce adds synthetic attributes (like unique identifiers) to each element and rewrites CSS selectors to scope styles to only those elements. It also patches DOM APIs like querySelector to restrict what elements a component can “see.”

The result feels like Shadow DOM to the developer — styles are scoped, DOM access is restricted — but it is all done through runtime JavaScript manipulation rather than a native browser feature.

The Differences That Matter

Here is a comparison of the three approaches:

FeatureLight DOMShadow DOM (Native)Synthetic Shadow DOM
Style encapsulationNoneFull (browser-enforced)Simulated (JavaScript-enforced)
DOM encapsulationNoneFullSimulated
External CSS appliesYesNoNo (with workarounds)
document.querySelector reaches insideYesNoNo (patched)
Element query from withinthis.querySelectorthis.template.querySelectorthis.template.querySelector
Performance overheadLowLow (native)Higher (polyfill overhead)
Browser supportUniversalModern browsersAll browsers (polyfill)

Salesforce has been migrating toward native Shadow DOM and away from Synthetic Shadow DOM. If you are building new components today, you should be aware that the platform is moving toward native Shadow DOM as the default, with Light DOM available as an opt-in alternative for specific use cases.

When to Use Which

Shadow DOM (the default for LWC) is the right choice for most components. It gives you clean encapsulation, prevents style leakage, and aligns with web standards. Use it unless you have a specific reason not to.

Light DOM is useful when your component needs to participate in a global styling context. For example, if you are building a component that will be rendered inside a CMS-managed page and needs to inherit the page’s styles, Light DOM makes that possible. It is also useful for SEO scenarios where you need the component’s content to be directly visible in the page DOM.

Synthetic Shadow DOM is what you get on older LWC implementations. You do not choose it directly — it is a platform behavior. If you are working on an org that has not yet been migrated to native Shadow DOM, your components will be running in Synthetic Shadow mode. The practical difference for most developers is minimal, but you may notice subtle behavior differences around event retargeting and style scoping edge cases.


What is Lightning Locker Service and Lightning Web Security?

Style and DOM encapsulation are only part of the story. Salesforce also needs to prevent components from doing malicious or unsafe things at runtime. This is where the platform’s security layers come in. There are two of them: the older Lightning Locker Service and the newer Lightning Web Security.

Lightning Locker Service

Lightning Locker Service (often just called “Locker Service” or “Locker”) was Salesforce’s original security architecture for Lightning components. It was introduced alongside Aura components and carried over to the early days of LWC.

Locker Service works by wrapping the global objects that JavaScript has access to — things like window, document, and XMLHttpRequest — in secure wrappers. These wrappers intercept calls to those objects and enforce restrictions. For example:

  • A component in one namespace cannot access the DOM elements of a component in a different namespace.
  • Access to document.cookie, window.localStorage, and other sensitive APIs is restricted.
  • Components cannot use eval() or new Function() to execute arbitrary code.
  • Inline event handlers in HTML are blocked.

The key mechanism is namespace isolation. Each component runs in a secure sandbox scoped to its namespace. If you have a component in the c namespace and another in a managed package namespace like myapp, Locker Service ensures that neither component can directly access the other’s DOM, data, or internal state.

┌─────────────────────────────────────────┐
│              Lightning Page              │
│                                          │
│  ┌──────────────────┐  ┌──────────────┐ │
│  │  c:myComponent   │  │ pkg:widget   │ │
│  │  ┌────────────┐  │  │ ┌──────────┐ │ │
│  │  │ Locker     │  │  │ │ Locker   │ │ │
│  │  │ Sandbox    │  │  │ │ Sandbox  │ │ │
│  │  │ (c namespace)│ │  │ │(pkg ns)  │ │ │
│  │  └────────────┘  │  │ └──────────┘ │ │
│  └──────────────────┘  └──────────────┘ │
└─────────────────────────────────────────┘

Each sandbox gets its own version of window and document that only exposes what the component is allowed to see.

Problems with Locker Service

Locker Service achieved its security goals, but it came with significant pain points:

Performance overhead. Wrapping every global object and intercepting every DOM call added runtime cost. Complex pages with many components could feel sluggish because of the overhead of the secure wrappers.

Compatibility issues. Many popular JavaScript libraries rely on direct access to window, document, or other global objects. Locker Service’s wrappers would break these libraries because the wrapped objects did not behave exactly like the native ones. Developers frequently ran into mysterious errors when trying to use third-party code.

Debugging difficulty. When something went wrong inside a Locker sandbox, the error messages were often cryptic. The secure wrappers added extra layers of indirection that made stack traces hard to read and breakpoints hard to set.

Restrictive APIs. Some legitimate use cases — like using eval for templating engines or accessing window.postMessage for cross-frame communication — were blocked entirely, even when they were not actually dangerous.

Lightning Web Security

Lightning Web Security (LWS) is Salesforce’s next-generation security architecture, designed to replace Locker Service. It addresses the same security concerns but uses a fundamentally different approach.

Instead of wrapping global objects in secure proxies, LWS uses JavaScript sandboxing through browser-native APIs. Specifically, it leverages a technique based on compartments (sometimes called “realms”) that creates isolated JavaScript execution environments without modifying the global objects themselves.

Here is how LWS differs from Locker Service:

Distortion instead of wrapping. LWS does not replace window and document with proxy objects. Instead, it modifies (or “distorts”) specific properties and methods that are dangerous while leaving everything else intact. This means that most JavaScript code works exactly as it would in a normal browser environment. Only the genuinely dangerous operations are intercepted.

Better library compatibility. Because LWS does not wrap global objects wholesale, third-party JavaScript libraries are far more likely to work correctly. The vast majority of libraries never touch the restricted APIs, so they run without modification.

Improved performance. By eliminating the proxy overhead and only intervening on specific dangerous operations, LWS has significantly less runtime cost than Locker Service.

Better error messages. When LWS does block an operation, the error messages are clearer and more directly tied to what the code was trying to do.

How LWS Works in Practice

When your LWC component runs under Lightning Web Security, the platform evaluates your component’s JavaScript in a sandboxed environment. The sandbox has a few key properties:

  1. Namespace isolation is maintained. Components in different namespaces still cannot access each other’s internals. This is the same security guarantee as Locker Service.

  2. DOM access is scoped. A component can only access DOM elements within its own shadow tree (or light DOM tree, if using Light DOM). It cannot reach into another component’s DOM.

  3. Dangerous APIs are distorted. Operations like eval(), document.cookie access from the wrong context, and direct window manipulation are blocked or modified.

  4. Safe APIs are untouched. Standard DOM methods, array methods, string methods, fetch API, and most of the JavaScript standard library work exactly as expected.

Here is a practical example. Under Locker Service, this code might fail because window is a proxy:

// Under Locker Service, this could fail
const width = window.innerWidth;
console.log(typeof window); // Might log "object" instead of expected behavior

Under LWS, the same code works normally because window is the real window object. Only the dangerous properties have been distorted:

// Under LWS, this works fine
const width = window.innerWidth; // Returns the real value
console.log(typeof window); // Logs as expected

// But this is still blocked
eval('alert("hello")'); // Throws a security error

The Migration from Locker to LWS

Salesforce is actively migrating the platform from Locker Service to Lightning Web Security. LWS is already the default for new LWC components, and Salesforce is working on expanding LWS support to Aura components as well.

For most developers, the migration is transparent. If your components followed Salesforce best practices and did not rely on Locker-specific quirks, they should work under LWS without changes. However, if you were using workarounds for Locker limitations — like specific patterns to access wrapped objects — you may need to update those patterns.

You can check whether your org is using LWS by going to Setup, searching for Session Settings, and looking for the Use Lightning Web Security for Lightning web components toggle.

How DOM Encapsulation and Security Work Together

It is worth stepping back and seeing how all these pieces fit together. When a Lightning Web Component renders on the page:

  1. The platform creates the component instance and renders its template.
  2. The component’s DOM is encapsulated using Shadow DOM (native or synthetic) or placed in Light DOM, depending on the component’s configuration.
  3. The component’s JavaScript runs inside a security sandbox (LWS or Locker Service) that restricts what global APIs the code can access.
  4. The combination of DOM encapsulation and JavaScript sandboxing ensures that each component is isolated from others — it cannot read their DOM, steal their data, or interfere with their behavior.

This layered approach is what makes it safe for Salesforce to run components from different vendors and different namespaces on the same page. A managed package component cannot read the DOM of your custom component, and your custom component cannot interfere with a Salesforce standard component. Everyone gets their own sandbox.


Section Notes

The DOM is the foundation that everything in LWC builds on. The browser parses your HTML templates into a tree of nodes, and your JavaScript interacts with that tree through the DOM API. But in Lightning Web Components, you never interact with the raw, global DOM directly. Your component’s markup lives inside a shadow tree (by default) that provides style and DOM encapsulation, and your JavaScript runs inside a security sandbox that prevents unsafe operations.

Shadow DOM is the default and the recommended approach for most components. It gives you clean isolation and prevents the kind of cross-component style and behavior conflicts that plague traditional web applications. Light DOM is available for specific use cases where you need global styles to reach your component’s markup. Synthetic Shadow DOM is the legacy polyfill that you may encounter on older implementations, and the platform is moving away from it.

On the security side, Lightning Web Security is the modern standard. It replaces the older Locker Service with a lighter, faster, more compatible approach to JavaScript sandboxing. If you are building new components, you are almost certainly running under LWS, and the experience is significantly better than what Locker Service offered.

In the next part, we will start building components that interact with the DOM in practice — handling events, querying elements within the shadow tree, and working with dynamic rendering. Now that you understand the environment your components live in, the patterns will make much more sense. See you there.