Light DOM

Lightning Web Components currently enforces shadow DOM on every component, encapsulating a component’s internal markup and making it inaccessible to programmatic code. When you use light DOM, your component lives outside the shadow DOM and avoids shadow DOM limitations. This approach eases third-party integrations and global styling.

Let's look at how the different DOM structures render in the DOM before we dive into details about light DOM. First, consider some markup for a shadow tree. Native shadow DOM renders components within the #shadow-root tag. However, Lightning Experience and Experience Cloud use synthetic shadow instead, which mimics native shadow behavior.

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.

1<my-app>
2  <my-header>
3    <p>Hello World</p>
4  </my-header>
5</my-app>

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

Light DOM provides several advantages over shadow DOM.

  • CSS theming and branding: Light DOM supports global styling, making it easy to apply custom branding to your components and child components.
  • Third-party tooling and testing: With light DOM, third-party tools can traverse the DOM, enabling standard browser query APIs like querySelector and querySelectorAll, without traversing the shadow root. For example, light DOM enables standard components in LWR sites to track events.
  • Accessibility: Light DOM doesn't scope IDs and enables two separate components to reference an ID on the other. For example, <label for="my-input"> can reference <input type="text" id="my-input"> even if the elements are in separate components.

The lwc-recipes repo has light DOM examples. Look for components that start with lightDom, such as lightDomQuery.

Tip

Guidelines for Working with Light DOM 

Using light DOM exposes your components to DOM scraping, so if you're working with sensitive data, we recommend using shadow DOM instead. In other words, light DOM doesn't provide 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.

Consider these best practices when using both light DOM and shadow DOM in your app.

  • You can nest a light DOM child component in a parent shadow DOM component and vice versa.
  • We recommend encapsulating deeply nested light DOM components in a single shadow DOM component at the top level. Then, you can share styles between all the components under the shadow root.
  • Be careful when querying elements or injecting styles at the document level. A shadow tree can sit between the document and your component.
  • You can override shadow DOM styles via CSS custom properties and ::part. However, the component owner is in charge of exposing the extension points, which prevents downstream consumers from styling arbitrary elements.

What's Not Available for Light DOM 

  • Restricting light DOM to specific namespaces isn’t supported.
  • Distributing components rendered in light DOM isn’t supported. Component references in a managed package use the c namespace and would result in a namespace conflict.
  • Base components are always rendered in shadow DOM.
  • Aura components can't use light DOM. However, an Aura component can contain an LWC component that uses light DOM.
  • Lifecycle hooks on slots are never invoked since the slot element does not render in the DOM.
  • Using slots inside a for:each iterator is not supported. For example:
1<!-- This results in a runtime error -->
2<template for:each={items} for:item="item">
3  <div key={item.id}>
4    <my-repeateditem item={item}>
5      <slot></slot>
6    </my-repeateditem>
7  </div>
8</template>

Lightning Locker Considerations 

Top-level light DOM components aren't protected by Lightning Locker or Lightning Web Security (LWS). Always nest light DOM components somewhere within a shadow DOM component. If you expose a light DOM component at the top-level of your component hierarchy, code from other namespaces can read the content of the component, for example.

Disabling Locker, such as in Experience Builder sites, means that you won't receive the security benefits from Locker. Make sure that you understand the ramifications of a more relaxed CSP environment on your Experience Builder site. See CSP and Lightning Locker Design Considerations.

In Aura-based Experience Builder sites, DOM access using this.querySelector() and other Web APIs are blocked by Lightning Locker. If you're using light DOM in Aura-based Experience Cloud sites, ensure that there's at least one LWC shadow DOM component as an ancestor to the light DOM component. Using a shadow DOM component means you encapsulate the component’s internal markup and are subject to shadow DOM limitations.

Beginning in Winter ’23, if Lightning Web Security (LWS) is enabled in the org, any Lightning web components contained in Aura sites are protected by LWS instead of Lightning Locker. If you disable Lightning Locker for the site, you also disable Lightning Web Security.

Note

Compare Light DOM and Shadow DOM 

Due to its strong encapsulation, shadow DOM is the recommended way to author components. It hides your component's internals so consumers can only use its public API.

Shadow DOM isn't suitable in the following cases.

  • Building a highly customizable UI, where you want complete control over a web app's appearance.
  • Using third-party libraries. Many popular libraries aren't compatible with shadow DOM.

Light DOM is a better fit in those cases, but note that consumers can access your component's internals as they can with your public API. Allowing such access makes it challenging to implement changes without impacting your consumer's code.

Here are pros and cons in using one over the other.

 Shadow DOMLight DOM
SecurityStrong component encapsulation protects components from unauthorized accessWeak encapsulation makes components open to unauthorized access
PortabilityHighly portable with access controlled through public APIsSusceptible to breaking changes caused by component authors or consumers
StylingRequires CSS custom properties to override stylesEasy to override styles
Third-party library and tool integrationLimited compatibility with third-party libraries or tools requiring DOM traversal or event retargetingSimple integration with third-party libraries and tools

When working with third-party libraries, such as Google Analytics or another instrumentation library, you don't have to use light DOM if your shadow DOM component exposes the right APIs. Let's say you want to instrument click interactions on a button:

1<!-- myButton.html example -->
2<template>
3  <button>{label}</button>
4</template>

With light DOM, you can attach a click event listener on the button element. If you render the component in shadow DOM, the button element isn’t accessible from outside the component. With shadow DOM, <button> becomes an internal implementation detail of the my-button component. In this case, the correct approach to instrument this component is to add a click handler on my-button itself to instrument it.

1<!-- myComponent.html example -->
2<template>
3  <my-button label="click me" onclick={handleClick}></my-button>
4</template>

Expose only the bare minimum you want to instrument since exposing your internal events can weaken your component's encapsulation. While the preceding example isn’t always possible, we recommend you explore your options before selecting the best fit for your use case.

Enable Light DOM in Your Component 

To enable light DOM, you start by setting the renderMode static property in your component class:

1import { LightningElement } from "lwc";
2
3export default class LightDomApp extends LightningElement {
4  static renderMode = "light"; // the default is 'shadow'
5}

Then use the lwc:render-mode root template directive, which is required for components using light DOM.

1<template lwc:render-mode="light">
2  <my-header>
3    <p>Hello World</p>
4  </my-header>
5</template>

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

Note

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 differences described in the following sections when you work with light DOM.

Composition 

Your app can contain components that use either shadow or light DOM. In this example template, my-app uses shadow DOM and contains several components: my-header uses light DOM and my-footer uses shadow DOM.

1<my-app>
2  #shadow-root 
3  | <my-header>
4  |   <p>Hello World</p>
5  | </my-header>
6  | <my-footer>
7  |   #shadow-root 
8  |   |  <p>Footer</p>
9  | </my-footer>
10</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 allows you to freely share styles between all child components within the one shadow root.

Tip

Accessibility 

Unlike synthetic or native shadow, light DOM doesn't scope IDs to an individual component. Instead, it enables a component to reference an ID on a separate component. This advantage enables you to link two elements using IDs and ARIA attributes by placing them in the same shadow root.

Consider this example with two sibling components.

1<!-- container.html -->
2<template>
3  <c-label></c-label>
4  <c-input></c-input>
5</template>

The c-label component contains a <label> element with an id attribute.

1<!-- label.html -->
2<template lwc:render-mode="light">
3  <label id="my-label">My label</label>
4</template>

The c-input component contains an <input> element that references the <label> element from the c-label component.

1<!-- input.html -->
2<template lwc:render-mode="light">
3  <input type="text" aria-labelledby="my-label" />
4</template>

CSS 

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

The styles on the following native shadow component cascades into the child component's light DOM. In this case, the light DOM component is inside the native shadow component and is mounted at the closest native shadow root level. This light DOM component is scoped locally within that entire shadow root and affects any light DOM components inside that root.

1<template>
2  <my-app>
3    #shadow-root
4    |  <style> p { color: green; }</style>
5    |  <p>This is a paragraph in shadow DOM</p>
6    |    <my-container>
7    |      <p>This is a paragraph in light DOM</p>
8    |    </my-container>
9  </my-app>
10</template>

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.

For synthetic shadow DOM, the shadow DOM styles don’t cascade into the light DOM child components.

In synthetic shadow DOM, styles are implemented at the global document level, but using attributes to scope the styles. This is a current limitation for synthetic shadow DOM.

Note

LWC doesn't scope styles automatically for you. To prevent styles from cascading out of a component, we recommend using scoped styles with *.scoped.css files. See the Use Scoped Styles in Light DOM section.

To override inherited styles in Lightning web components, use SLDS styling hooks in the component stylesheet. Your styling hooks act as placeholders for your custom styles. See the Blueprint Overview for a list of component blueprints that support component styling hooks.

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.

Tip

Access Elements 

In shadow DOM, you can only access elements the component owns.

In contrast, 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').

1<!-- JS code returns "Your Content Here" -->
2<template>
3  <script>
4    console.log(document.querySelector("my-custom-class").textContent);
5  </script>
6  <my-component>
7    <div class="my-custom-class">Your Content Here</div>
8  </my-component>
9</template>

With shadow DOM, LightningElement.prototype.template returns the component-associated shadow root. The template element isn't available to components that use light DOM, so 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.

1import { LightningElement } from "lwc";
2
3export default class LightDomApp extends LightningElement {
4  static renderMode = "light";
5  query(event) {
6    const el = this.querySelector("p");
7    const all = this.querySelectorAll("p");
8    const elById = this.getElementById("#myId");
9    const elements = this.getElementsByClassName("my-class");
10    const tag = this.getElementsByTagName("button");
11  }
12}

With light DOM components, this.querySelectorAll() can return elements rendered by other light DOM components. To access the parent element in light DOM, use this.hostElement.

The id attribute on an element is preserved at runtime and isn’t manipulated as in synthetic shadow DOM. So you can use an id selector in CSS or JavaScript because it matches the element's id at runtime.

Alternatively, use this.refs when you're working with components in light DOM and shadow DOM. this.refs accesses the element that's defined in the component and behaves similarly in both light DOM and shadow DOM, unlike this.querySelector or this.querySelectorAll.

Note

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 aren’t 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 component uses shadow DOM.

1<!-- app.html (shadow DOM) -->
2<template>
3  <c-light-container onbuttonclick={handleButtonClick}> </c-light-container>
4</template>
1<!-- lightContainer.html -->
2<template lwc:render-mode="light">
3  <p>Hello, Light DOM Container</p>
4  <!-- c-light-child host -->
5  <c-light-child onbuttonclick={handleButtonClick}> </c-light-child>
6</template>
1// lightContainer.js
2import { LightningElement } from "lwc";
3
4export default class LightContainer extends LightningElement {
5  static renderMode = "light";
6  handleButtonClick(event) {
7    // do something
8  }
9}
1<!-- lightChild.html -->
2<template lwc:render-mode="light">
3  <button onclick={handleClick}></button>
4</template>
1// lightChild.js
2import { LightningElement } from "lwc";
3
4export default class LightChild extends LightningElement {
5  static renderMode = "light";
6  handleClick(event) {
7    this.dispatchEvent(new CustomEvent("buttonclick", { bubbles: true, composed: false }));
8  }
9}

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

c-light-child host handler

  • event.currentTarget: c-light-child
  • event.target: c-light-child

c-light-container host handler

  • event.currentTarget: c-light-container
  • event.target: c-light-child

In contrast, if c-light-container uses shadow DOM, the event doesn’t escape the shadow root.

Events bubble through components even if composed is false in light DOM, since there's no shadow root.

Note

Slots 

Light DOM emulates slots because browsers don't support them outside shadow DOM. Slots in light DOM behave similarly to synthetic shadow slots. LWC determines at run-time if a slot is running light DOM.

The <slot> element is a placeholder in a component for where slotted content gets attached. <slot> isn't rendered in light DOM, so if other parts of your code depend on attributes or event listeners of a <slot> element, LWC throws a compiler error. For example, the slotchange event and ::slotted CSS pseudo-selector aren’t supported since the slot element doesn’t render in the DOM.

A <slot> element without the name property is a default (or unnamed) slot. To pass content from only a specific element into a <slot>, create a named slot.

Named Slots 

To create a named slot, set a <slot> element's name property to a string value. To insert slotted content in that named slot, set the slot property of the element with content to the same string value.

In the sample code below, the component my-component has a named and an unnamed slot. The named slot contains fallback content that only renders if you don't provide slotted content.

1<!-- myComponent.html -->
2<template>
3  <slot name="title">Fallback content</slot> // Named slot
4  <h3>Heading text</h3>
5  <slot></slot> // Unnamed/Default slot
6</template>

Pass slotted content into my-component like this.

1<my-component>
2  <p>Default slotted content</p>
3  <h1 slot="title">Content for named slot</h1>
4</my-component>

At run-time, the slotted content or fallback content is flattened to the parent element. Content is directly appended to the host element in the DOM. The <slot> elements in my-component aren’t rendered to the DOM.

1<my-component>
2  <h1>Content for named slot</h1>
3  <h3>Heading text</h3>
4  <p>Default slotted content</p>
5</my-component>

Light DOM only renders slotted elements that are assigned to slots. If an element with slotted content isn't linked to a slot, the element's lifecycle hooks are never invoked.

1<!-- c-parent -->
2<template>
3  <c-child>
4    <span>This element isn't rendered in light DOM because c-child doesn't have a slot</span>
5  </c-child>
6</template>
7
8<!-- c-child -->
9<template>
10  <p>No slot here</p>
11</template>

Slotted Content and Components 

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.

1<!-- my-component -->
2<template>
3  <slot name="content">Default content in the named slot</slot>
4  <p>This makes the component a bit more complex</p>
5  <slot>This is a default slot to test if content bypasses the named slot and goes here</slot>
6</template>

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

1<my-component>
2  <!-- Inserted into the content slot -->
3  <div slot="content">Some text here</div>
4</my-component>
5
6<my-component>
7  <!-- Inserted into the content slot -->
8  <my-shadow-lwc slot="content">Some text here</my-shadow-lwc>
9</my-component>
10
11<my-component>
12  <!-- Inserted into the content slot -->
13  <my-light-lwc slot="content">Some text here</my-light-lwc>
14</my-component>
15
16<my-component>
17  <!-- Inserted into the default slot -->
18  <my-shadow-lwc>Some text here</my-shadow-lwc>
19</my-component>
20
21<my-component>
22  <!-- Inserted into the default slot -->
23  <my-light-lwc>Some text here</my-light-lwc>
24</my-component>

Style Inheritance for Slots 

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.

1<!-- app.html -->
2<template>
3  <c-shadow-component></c-shadow-component>
4  <c-light-slot-consumer></c-light-slot-consumer>
5</template>
1<!-- lightSlotConsumer.html -->
2<template lwc:render-mode="light">
3  <c-shadow-slot-container>
4    <p>Hello from shadow slot</p>
5  </c-shadow-slot-container>
6  <c-light-slot-container>
7    <p>Hello from light slot</p>
8  </c-light-slot-container>
9</template>
1<!-- shadowSlotContainer.html -->
2<template>
3  <slot></slot>
4</template>
1<!-- lightSlotContainer.html -->
2<template lwc:render-mode="light">
3  <slot name="other">
4    <p>Hello from other slot</p>
5  </slot>
6  <slot>This is the default slot</slot>
7</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, the shadow DOM component without slots doesn't receive the styles.

1<c-app>
2  <style type="text/css">
3    p {
4      background: green;
5      color: white;
6    }
7  </style>    
8  <h2>Hello Light DOM</h2>
9    <p>This is a paragraph in app.html</p>
10    <h3>Shadow DOM</h3>
11    <c-shadow-component>
12        #shadow-root (open)
13        | <p>Hello, Shadow DOM container</p>
14    </c-shadow-component>
15    <h3>Slots</h3>
16    <c-light-slot-consumer>
17        <c-shadow-slot-container>
18            #shadow-root (open)
19            | <p>Hello from shadow-slot-container</p>
20        </c-shadow-slot-container>
21        <c-light-slot-container>
22            <p>Hello from other slot</p>
23            <p>Hello from light-slot-container</p>
24        </c-light-slot-container>
25    </c-light-slot-consumer>
26</c-app>

slot Attribute for Light DOM Elements 

When a component is rendered to the DOM at runtime, light DOM slots don't rely on the native browser <slot> elements or slot attributes.

In LWC API v61.0 and later:

LWC removes the slot attribute from an element when it's being slotted into a light DOM slot.

For example, this <template> passes a <div> with the slot='lightSlot' property into a light DOM slot.

Example <template>
1<template>
2    <c-light-child>
3        <div slot='lightSlot'>I got slotted</div>
4    </c-light-child>
5</template>
c-light-child
1<template render:mode="light">
2    <slot name="lightSlot"></slot>
3</template>

The c-light-child component renders to the DOM without the slot property.

DOM output for <template>
1<c-light-child>
2    <div>I got slotted</div>    // div renders WITHOUT the slot property
3</c-light-child>

To support slot forwarding, native and synthetic shadow slots still preserve the slot attribute. For example, say you have a light DOM slot that forwards a <div> with slot="lightSlot" into a shadow DOM slot.

Example <template>
1<template>
2    <c-light-slot>
3        <div slot="lightSlot">Slotted content from template</div>
4    </c-light-slot>
5</template>
c-light-slot
1<template>
2    <c-shadow-slot>
3        <slot name="lightSlot" slot="shadowSlot"></slot>
4    <c-shadow-slot>
5</template>
c-shadow-slot
1<template>
2    <slot name="shadowSlot"></slot>
3</template>

The slotted content (<div>) renders in the DOM with its slot attribute preserved.

1<c-light-slot>
2    <c-shadow-slot>
3        // The div preserves the slot attribute from c-light-slot
4        <div slot="shadowSlot">Slotted content from template</div>
5    </c-shadow-slot>
6</c-light-slot>

If your CSS selectors or querySelector() methods reference the slot attribute, we recommend you choose a different CSS selector or use lwc:ref instead.

1<!-- Do not rely on CSS selectors like this -->
2const foo = this.querySelector('[slot="foo"]');
3
4<!-- Instead, use lwc:ref -->
5<c-child>
6    <div slot="foo" lwc:ref="foo">hello</div>
7</c-child>

You should also review your Jest snapshots to see if they're impacted by this change.

In LWC API v60.0 and earlier:

Light DOM elements render with their slot attributes. The example <template> above renders to the DOM as follows.

Original DOM output for <template>
1<div slot="namedSlot">hello</div>

Slot Forwarding 

Slot forwarding refers to passing content from one slot to another slot.

For example, say you have a c-outer component that pulls content from a <span> element into its named slot namedSlot. c-outer also has a child component, c-inner. You want to forward content from namedSlot into c-inner's slot forwardedSlot.

Diagram of how content is forwarded from one slot to another.

container.html passes the <span> with slotted content into <c-outer>.

1<!-- container.html -->
2<template>
3  <c-outer>
4    <span slot="namedSlot">Named slot content</span>
5  </c-outer>
6</template>

In LWC API v61.0 and later:

You can forward content from a parent's light DOM slot to a child's slot using only the slot attribute. You no longer have to create a wrapper around any of the slots.

For these versions of LWC, here's an example of a valid c-outer component.

1<!-- outer.html -->
2<template lwc:render-mode="light">
3  <c-inner>
4    <slot name="namedSlot" slot="forwardedSlot"></slot>
5  </c-inner>
6</template>

namedSlot receives content from the <span> in container.html because it has the property name="namedSlot". Additionally, namedSlot passes its content to the slot named forwardedSlot because it has the slot="forwardedSlot property.

The "Named slot content" string renders in the slot named forwardedSlot for <c-inner>.

1<!-- inner.html -->
2<template>
3  <slot name="forwardedSlot"></slot>
4</template>

In LWC API v60.0 and earlier:

To forward light DOM slot content into another slot, you have to wrap the outermost <slot> element in another element and assign the slot attribute to the element.

Here's an example of a c-outer component with a <div> wrapper around namedSlot.

1<!-- outer.html -->
2<template lwc:render-mode="light">
3  <c-inner>
4    <!-- wrapper div -->
5    <div slot="forwardedSlot">
6      <slot name="namedSlot"></slot>
7    </div>
8  </c-inner>
9</template>

The same applies to default slots.

1<!-- outer.html -->
2<template lwc:render-mode="light">
3  <c-inner>
4    <!-- wrapper div -->
5    <div>
6      <slot name="namedSlot"></slot>
7    </div>
8  </c-inner>
9</template>

As a result, the "Named slot content" string renders in the forwardedSlot slot for <c-inner>.

1<!-- inner.html -->
2<template>
3  <slot name="forwardedSlot"></slot>
4</template>

Scoped Slots 

With scoped slots, you can access data in a child component and render it in slotted content inside of a parent component. Binding data from the child component to the scoped slot allows the parent component to reference the child component’s data in the slotted content. This data is slotted in the child component’s light DOM.

In this example, the child component <c-child> binds its item data to the scoped slot <slot>. In the parent component <c-parent>, the scoped slot fragment references {item.id} and {item.name} in the markup that will be slotted to <c-child>.

1<!-- c/parent.html -->
2<template> <!-- Parent component doesn’t need to be light DOM -->
3    <c-child>
4        <template lwc:slot-data="item">
5            <span>{item.id} - {item.name}</span>
6        </template>
7    </c-child>
8</template>
9
10
11<!-- c/child.html -->
12<template lwc:render-mode="light"> <!-- Child must be light DOM -->
13    <ul>
14        <template for:each={item} for:item="item">
15            <li key={item.id}>
16                <slot lwc:slot-bind={item}</slot>
17            </li>
18        </template>
19    </ul>
20</template>

Because the scoped slot fragment is in the parent component’s template, the parent component owns the slotted content. So if the parent component references a scoped style sheet, those styles also apply to the content of the scoped slot.

The parent component partially renders the content of the scoped slot, so it must be enclosed in <template></template> tags. In the example, the scoped slot content is <span>{item.id} - {item.name}</span>. The parent component creates this partial fragment.

The parent component also renders each item, and the child component controls the loop logic. <c-child> creates as many slots as needed using the same template fragment passed from <c-parent>.

To use scoped slots, the child component must use light DOM. Scoped slots in shadow DOM aren’t supported. The parent can be a light DOM or shadow DOM component.

Note

The final HTML looks as follows.

1<c-parent>
2  #shadow-root
3  | <c-child>
4  |  <ul>
5  |    <li>
6  |      <span>1 - One</span>
7  |    </li>
8  |    <li>
9  |      <span>2 - Two</span>
10  |    </li>
11  |  </ul>
12  | </c-child>
13</c-parent>

To introduce scoped slots into your components, add the directives lwc:slot-bind and lwc:slot-data. For more information, see Directives for Slots and Directives for Nested Templates.

Multiple Scoped Slots and Bindings 

A child component can have multiple named scoped slots, but it can have only one default scoped slot.

1<template>
2    <c-child>
3        <template lwc:slot-data="defaultdata"> <!-- This is a default slot -->
4            <p>{defaultdata.title}</p>
5        </template>
6        <template slot="slotname1" lwc:slot-data="slot1data"> <!-- This is a named slot -->
7            <p>{slot1data.title}</p>
8        </template>
9        <template slot="slotname2" lwc:slot-data="slot2data"> <!-- This is a named slot -->
10            <p>{slot2data.title}</p>
11        </template>
12    </c-child>
13</template

You can bind different scoped slots to the same source of data. In the example below, the default scoped slot and the two named scoped slots render content from slotdata.

1<template lwc:render-mode="light">
2  <!-- This is a default slot -->
3  <slot lwc:slot-bind={slotdata}></slot>
4  <!-- This is a named slot -->
5  <slot name="slotname1" lwc:slot-bind={slotdata}></slot>
6  <!-- This is a named slot -->
7  <slot name="slotname2" lwc:slot-bind={slotdata}></slot>
8</template>

You can bind a scoped slot to only one source of data. For example, binding the named scoped slot namedslotA to two different sets of data, slot1data and slot2data, results in a compiler error.

1<!-- Invalid usage of named scoped slot -->
2<template lwc:render-mode="light">
3  <slot name="namedslotA" lwc:slot-bind={slot1data}></slot>
4  <slot name="namedslotA" lwc:slot-bind={slot2data}></slot>
5</template>

If you try to bind a default scoped slot to multiple different sets of data, the compiler throws the same error.

1<!-- Invalid usage of default scoped slot -->
2<template lwc:render-mode="light">
3  <slot lwc:slot-bind={slot1data}></slot>
4  <slot lwc:slot-bind={slot2data}></slot>
5</template>

Mixing Standard and Scoped Slots 

Because you can only have one default slot in a component, you can’t place a standard default slot and a default scoped slot in the same component. The following code results in an error.

1<!-- c/child.html -->
2<!-- Invalid usage of default slots -->
3<template lwc:render-mode="light">
4  <slot lwc:slot-bind={slotdata}>Default scoped slot</slot>
5  <slot>Standard default slot</slot>
6</template>

Within a child component, a named scoped slot and a standard named slot can’t share the same name. The following code results in an error.

1<!-- c/child.html -->
2<!-- Invalid usage of named slots -->
3<template lwc:render-mode="light">
4  <slot name="slotname1" lwc:slot-bind={slotdata}>Named scoped slot</slot>
5  <slot name="slotname1">Standard named slot</slot>
6</template>

When binding a scoped slot in a parent component to data from a child component, the components must contain the same type of slot. For example, if a parent component contains a scoped slot bound to a child component, that child component must also have a scoped slot. Otherwise, the slotted content isn’t rendered. If you enable debug mode, an error is also logged in the dev console.

Flexibility of Scoped Slots 

You can nest a scoped slot inside another scoped slot.

1<template>
2  <c-table data={data}>
3    <template lwc:slot-data="row">
4      <c-row row={row}> <!-- This is rendered for every row in the table -->
5        <template lwc:slot-data="column">
6          <span> <!-- This is rendered for every column in the row -->
7            Coordinates: {row.number} - {column.number} <!-- This can refer to both `row` and `column` -->
8          </span>
9        </template>
10      </c-row>
11    <template>
12  </c-table>
13</template>

Scoped slots can reference component bindings and scope bindings.

1<template>
2  {title}
3  <c-list>
4    <template lwc:slot-data="item">
5      <div>{label}</div>
6      <!-- label is a component binding that’s repeated in every row of the list -->
7      <span>{item.id} - {item.name}</span>
8    </template>
9  </c-list>
10</template>

Use Scoped Styles in Light DOM 

In light DOM, you can use scoped styles to apply CSS to elements on the component only. This behavior is similar to style encapsulation with shadow DOM.

To add scoped styles to a component, create a *.scoped.css file in the component folder.

1myCmp
2    ├──myCmp.html
3    ├──myCmp.css
4    └──myCmp.scoped.css

You can include either one, both, or neither of the CSS files in the example above for shadow DOM components and light DOM components.

In Aura-based containers, light DOM components can only load scoped styles. For example, in Aura-based Experience Builder sites, you must include the *.scoped.css file for your custom components instead of the *.css file.

Note

Let's examine a light DOM component with scoped styles.

1<!-- lightCmp.html -->
2<template lwc:render-mode="light">
3  <p>This is a paragraph in c-light-cmp</p>
4</template>
1// lightCmp.js
2import { LightningElement } from "lwc";
3
4export default class LightCmp extends LightningElement {
5  static renderMode = "light";
6}
1/* lightCmp.scoped.css */
2p {
3  background: silver;
4  color: black;
5}

The scoped style results in:

1<c-light-cmp class="c-lightCmp_lightCmp-host">
2  <style class="c-lightCmp_lightCmp" type="text/css">
3    p.c-lightCmp_lightCmp {
4      background-color: silver;
5      color: black;
6    }
7  </style>
8  <p class="c-lightCmp_lightCmp">This is a paragraph in c-light-cmp</p>
9</c-light-cmp>

If a *.css file is used with a *.scoped.css file, the *.css stylesheets are injected before the *.scoped.css stylesheets. The scoped style CSS selectors have precedence over the unscoped ones because they’re declared last.

If a scoped stylesheet and an unscoped are used on the template, both style sheets are applied. The injection order influences which styles are applied by the browser when you have duplicated selectors. See tree proximity ignorance.

Note

1/* lightCmp.css */
2p {
3  background: yellow;
4  color: #777;
5}

In this case, c-light-cmp uses the scoped styles, but the styles from the unscoped stylesheet can bleed out of the component.

1<!-- c-app -->
2<template>
3  <!-- This paragraph is styled yellow from lightCmp.css -->
4  <p>This is a paragraph in c-app shadow DOM</p>
5  <c-light-cmp></c-light-cmp>
6</template>

In the preceding example, the paragraph in c-app inherits the styles from lightCmp.css. To override the styles from lightCmp.css, include a scoped stylesheet app.scoped.css.

We don't recommend using the !important rule as it makes debugging more difficult. When you use !important on a style declaration in both your scoped and unscoped stylesheet, the scoped style has greater specificity and its style is applied to the component.

Note

Scoped Region 

When you use *.scoped.css, the CSS selectors are scoped to all elements in the component HTML file.

1<!-- c-my-cmp -->
2<template>
3  <div>
4    <span></span>
5    <button class="my-button"></button>
6  </div>
7</template>

In the following CSS, all selectors match the elements in the template, and only those elements.

1/* myCmp.css */
2div {
3}
4span {
5}
6button {
7}
8div > span {
9}
10div button {
11}
12.my-button {
13}

The root element c-light-cmp can be targeted using :host, even in a light DOM component.

CSS scoping uses custom CSS classes to prevent styles from leaking out of the component.

Target the Root Element 

As light DOM styles aren't scoped by default, the :host pseudo selector refers to the closest shadow root host element (if any). With light DOM scoped styles, the :host selector refers to the root element of the light DOM component, which is the root of the scoped DOM region.

Let's say you have an unscoped and scoped stylesheet on a light DOM component.

1/* light.css */
2:host {
3  background: red;
4}
1/* light.scoped.css */
2:host {
3  background: blue;
4}

The component renders:

1<c-shadow>
2  <!-- red background -->
3  #shadow-root
4  <c-light class="c-light_light-host">
5    <!-- blue background -->
6    <style>
7      :host {
8        background: red;
9      }
10      :c-light_light-host {
11        background: blue;
12      }
13    </style>
14  </c-light>
15</c-shadow>

In the preceding example, :host is transformed for the scoped style.

The :host-context() selector isn’t supported.

Note

Compare Scoped Styles with Synthetic Shadow DOM 

Light DOM scoped styles have some differences from LWC's synthetic shadow scoped styles.

  • Light DOM scoped styles use classes for scoping, but synthetic shadow DOM uses HTML attributes.
  • Light DOM scoped styles don't support @import within scoped stylesheets.
  • Light DOM scoped styles don't apply to content that's manually injected into the template inside of lwc:dom="manual", for example, content injected using Element.appendChild or Element.innerHTML.

Convert Components from Shadow DOM to Light DOM 

To easily convert components from shadow DOM to light DOM, use the lwc-codemod tool. The tool modifies the files in a component bundle, which includes adding the renderMode property in the JavaScript file and the lwc:render-mode directive in the HTML file.

For more information, see the lwc-codemod documentation.

See Also