Light DOM (Developer Preview)

The framework enforces shadow DOM on every component. Shadow DOM encapsulates your component’s internal markup, which makes it inaccessible to programmatic code, therefore posing challenges for some third-party integrations and global styling. Contrastingly, light DOM enables your component markup to live outside the shadow DOM.

Note

Light DOM is available currently as a developer preview for LWC Open Source only. This means all commands, parameters, and behaviors are subject to change or deprecation at any time, with or without notice.

Let's look at how the different DOM structures render in the DOM before we dive into light DOM. Although both synthetic and native shadow behave similarly, LWC OSS uses native shadow, which appears as #shadow-root (open) tag when you inspect it.

<!-- my-app -->
<my-app>
    #shadow-root (open)
    |    <my-header>
    |        #shadow-root (open)
    |        |    <p>Hello World</p>
    |    </my-header>
</my-app>

With light DOM, the component content is attached to the host element instead of its shadow tree. It can then be accessed like any other content in the document host, providing similar behavior to content that's not bound by shadow DOM.

<my-app>
    <my-header>
        <p>Hello World</p>
    </my-header>
</my-app>

For a comprehensive overview, see Google Web Fundamentals: Shadow DOM v1.

Light DOM provides several advantages over shadow DOM.

Considerations and Limitations

The Light DOM developer preview is still under development and has a number of limitations. You don't get the benefits that come with shadow DOM encapsulation, which prevents unauthorized access into the shadow tree. Since the DOM is open for traversal by other components and third-party tools, you are responsible for securing your light DOM components.

Additionally, note that:

Enable Light DOM

Set the runtime tag lwcRuntimeFlags.ENABLE_LIGHT_DOM_COMPONENTS to true.

<!-- This script can run before or after LWC loads -->
<script>
    window.lwcRuntimeFlags = window.lwcRuntimeFlags || {};
    window.lwcRuntimeFlags.ENABLE_LIGHT_DOM_COMPONENTS = true;
</script>

Set the renderMode static field in your component class.

import { LightningElement } from 'lwc';

export default class LightDomApp extends LightningElement {
    static renderMode = 'light'; // the default is 'shadow'
}

Use the required lwc:render-mode root template directive for components that are using light DOM.

<template lwc:render-mode='light'>
    <my-header>
        <p>Hello World</p>
    </my-header>
</template>

Note

Changing the value of the renderMode static property after instantiation doesn't impact whether components render in light DOM or shadow DOM.

Work with Light DOM

Migrating a component from shadow DOM to light DOM requires some code changes. The shadow tree affects how you work with CSS, events, and the DOM. Consider the following differences when you work with light DOM.

Composition

Your app can contain components that use either shadow or light DOM. For example, my-header uses light DOM and my-footer uses shadow DOM.

<my-app>
    #shadow-root (open)
    |    <my-header>
    |        <p>Hello World</p>
    |    </my-header>
    |    <my-footer>
    |        #shadow-root (open)
    |        |    <p>Footer</p>
    |    </my-footer>
</my-app>

A light DOM component can contain a shadow DOM component. Similarly, a shadow DOM component can contain a light DOM component. If you have deeply-nested components, consider a single shadow DOM component at the top-level with nested light DOM components. This structure reduces the overhead of using shadows within nested components.

CSS

With shadow DOM, CSS styles defined in a parent component don’t cascade into a child. However, light DOM enables styling from the parent document to target a DOM node and style it.

The styles on the following shadow component cascades into the child component's light DOM.

<my-app>
    #shadow-root (open)
    |    <style> p { color: green; }</style>
    |    <p>This is a paragraph in shadow DOM</p>
    |    <my-container>
    |        <p>This is a paragraph in light DOM</p>
    |    </my-container>
</my-app>

Similarly, the styles on a child component rendered in light DOM are applied to its parent components until a shadow boundary is encountered when using native shadow DOM.

Note

In synthetic shadow DOM, styles are applied globally like with light DOM. This is a current limitation for synthetic shadow DOM.

LWC doesn't scope styles automatically for you. To prevent styles from bleeding in or out of a component, scope your styles using classes or attributes and write specific selectors to target your elements.

<my-app>
    #shadow-root (open)
    |    <p>This is a paragraph in shadow DOM</p>
    |    <my-container>
    |        <style type="text/css">
    |            p.highlight { background: yellow; }
    |        </style>
    |        <p class="highlight">This is a paragraph in light DOM</p>
    |    </my-container>
</my-app>

Tip

The order in which light DOM components are rendered impacts the order in which stylesheets are injected into the root node and directly influences CSS rule specificity.

Access Elements

You can retrieve a node from a light DOM component, which is helpful for third-party integrations and testing. For example, you can query the paragraph in your app using document.querySelector('p').You can't do this with shadow DOM.

<!-- JS code returns "Your Content Here" -->
<script>
    console.log(document.querySelector('my-custom-class').textContent)
</script>
<my-component>
    <div class="my-custom-class">Your Content Here</p>
</my-component>

With shadow DOM, LightningElement.prototype.template returns the component-associated ShadowRoot. With Light DOM, LightningElement.prototype.template returns null.

When migrating a shadow DOM component to light DOM, replace this.template.querySelector with this.querySelector. The following example uses a list of common DOM APIs to work with a light DOM component.

import { LightningElement } from 'lwc';

export default class LightDomApp extends LightningElement {
    static renderMode = 'light';
    query(event) {
        const el = this.querySelector('p');
        const all = this.querySelectorAll('p');
        const elById = this.getElementById('#myId');
        const elements = this.getElementsByClassName('my-class');
        const tag = this.getElementsByTagName('button');
    }
}

With light DOM components, this.querySelectorAll() can return elements rendered by other components.

Note

The ID on an element is preserved at runtime and they are not manipulated like in synthetic shadow DOM.

Events

With shadow DOM, if an event bubbles up and crosses the shadow boundary, some property values change to match the scope of the listener. With light DOM, events are not retargeted. If you click a button that's nested within multiple layers of light DOM components, the click event can be accessed at the document level. Also, event.target returns the button that triggered the event instead of the containing component.

For example, you have a component c-light-child using light DOM nested in a container component c-light-container that's also using light DOM. The top-level c-app uses shadow DOM.

<!-- c-app (shadow DOM) -->
<template>
    <c-light-container onbuttonclick={handleButtonClick}> 
    </c-light-container>
</template>
<!-- c-light-container -->
<template lwc:render-mode="light">
    <p>Hello, Light DOM Container</p>
    <!-- c-light-child host -->
    <c-light-child onbuttonclick={handleButtonClick}>
    </c-light-child>
</template>
<!-- c-light-child -->
<template lwc:render-mode="light">
    <button onclick={handleClick}>
    </button>
</template>
// lightChild.js
import { LightningElement } from 'lwc';

export default class LightChild extends LightningElement {
    static renderMode = 'light';
    handleClick(event) {
        this.dispatchEvent(
            new CustomEvent('buttonclick',
                { bubbles: true, composed: false }
            )
        );
    }
}

When you dispatch the custom buttonclick event in c-light-child, the handlers return the following elements.

c-light-child host handler

c-light-container host handler

Contrastingly, if c-light-container were to use shadow DOM, the event doesn’t escape the shadow root.

Slots

Slots are emulated in light DOM since there is no browser support for slots outside of shadow DOM. Slots in light DOM behave similarly to synthetic shadow slots. LWC determines at runtime if a slot is running light DOM.

Let's say you have a component my-component with a named and unnamed slot.

<!-- my-component.html -->
<template>
   <slot name="title"></slot>
   <h3>Subtitle</h3>
   <slot></slot>
</template>

Use the component like this.

<my-component>
   <p>Default slotted content</p>
   <h1 slot="title">Component Title</h1>
</my-component>

These slots are not rendered to the DOM. The content is directly appended to the host element in the DOM.

<my-component>
   <h1>Component Title</h1>
   <h3>Subtitle</h3>
   <p>Default slotted content</p>
</my-component>

The slotted content or fallback content is flattened to the parent element at runtime. The <slot> element itself isn't rendered, so adding attributes or event listeners to the <slot> element throws a compiler error.

Additionally, consider this light DOM component c-light-slot-consumer that contains a shadow DOM component c-shadow-slot-container and light DOM component c-light-slot-container.

<!-- c-app -->
<template>
   <c-shadow-component></c-shadow-component>
   <c-light-slot-consumer></c-light-slot-consumer>
</template>
<!-- c-light-slot-consumer -->
<template lwc:render-mode="light">
   <c-shadow-slot-container>
       <p>Hello from shadow slot</p>
   </c-shadow-slot-container>
   <c-light-slot-container>
        <p>Hello from light slot</p>
   </c-light-slot-container>
</template>
<!-- c-shadow-slot-container -->
<template>
   <slot></slot>
</template>
<!-- c-light-slot-container -->
<template lwc:render-mode="light">
   <slot name="other">
       <p>Hello from other slot</p>
   </slot>
   <slot>This is the default slot</slot>
</template>

If you include styles in c-app, all elements within the slots (in both the shadow DOM and light DOM components) get the styles. However, notice that the shadow DOM component c-shadow-component without slots doesn't receive the styles.

<c-app>
    <style type="text/css">p { background: green;color: white; }</style>
    <h2>Hello Light DOM</h2>
    <p>This is a paragraph in app.html</p>
    <h3>Shadow DOM</h3>
    <c-shadow-component>
        #shadow-root (open)
        |    <p>Hello, Shadow DOM container</p>
    </c-shadow-component>
    <h3>Slots</h3>
    <c-light-slot-consumer>
        <c-shadow-slot-container>
            #shadow-root (open)
            |    <p>Hello from shadow-slot-container</p>
        </c-shadow-slot-container>
        <c-light-slot-container>
            <p>Hello from other slot</p>
            <p>Hello from light-slot-container</p>
        </c-light-slot-container>
    </c-light-slot-consumer>
</c-app>

Consider these composition models using slots. A component in light DOM can slot in content and other components. The slots support both light DOM and shadow DOM components.

<template>
    <slot name="content">Default content in the named slot</slot>
    <p>This makes the component a bit more complex</p>
    <slot>This is a default slot to test if content bypasses the named slot and goes here</slot>
</template>

Here's how your content is rendered in the slots.

<my-component>
    <!-- Inserted into the content slot -->
    <div slot="content">Some text here</div>
</my-component>

<my-component>
    <!-- Inserted into the content slot -->
    <my-shadow-lwc slot="content">Some text here</my-shadow-lwc>
</my-component>

<my-component>
    <!-- Inserted into the content slot -->
    <my-light-lwc slot="content">Some text here</my-light-lwc>
</my-component>

<my-component>
    <!-- Inserted into the default slot -->
    <my-shadow-lwc>Some text here</my-shadow-lwc>
</my-component>

<my-component>
    <!-- Inserted into the default slot -->
    <my-light-lwc>Some text here</my-light-lwc>
</my-component>

Note

The slotchange event and ::slotted CSS pseudo-selector are not supported since the slot element does not render in the DOM.

Light DOM doesn't render slotted elements that are not assigned to a slot, which means that their lifecycle hooks are never invoked.

<!-- c-parent -->
<template>
    <c-child>
        <span>This element is not rendered in light DOM</span>
    </c-child>
</template>

<!-- c-child -->
<template>
    <p>This component does not include a slot</p>
</template>

Feedback and Comments

We are continuously collecting usage metrics during this developer preview to better understand the behavior and performance of light DOM in our customers' solutions. If you have existing LWC components that benefit from using light DOM, feel free to share feedback on any light DOM conversion issues you experience.

Your feedback is welcome! Provide your comments, report bugs, or make feature requests in the LWC repo.