Build Components in Mixed Shadow Mode (Beta)

Mixed shadow mode enables a component to use native shadow DOM, even when the synthetic shadow polyfill is applied.

This feature is a Beta Service. Customer may opt to try such Beta Service in its sole discretion. Any use of the Beta Service is subject to the applicable Beta Services Terms provided at Agreements and Terms.

All major browsers now support shadow DOM. Salesforce maintains the synthetic shadow polyfill for legacy browsers and is committed to migrating to native shadow DOM via mixed shadow mode. With mixed shadow mode, you gain the speed and efficiency of using native shadow as much as possible in your app. And you can more readily migrate to use native shadow fully in the future.

Our benchmarks have shown that native shadow components are up to 50% faster than their synthetic counterparts in some scenarios. In the future, removing the synthetic shadow polyfill can reduce LWC's overall JavaScript size by half. Mixed shadow mode takes us closer to being able to fully migrate to native shadow, then enabling us to remove the polyfill from LWC completely.

Native and synthetic shadow have several differences, such as how they achieve encapsulation through slots versus styling. While apps rely on global stylesheets to apply styles in synthetic shadow DOM, a component's styles are added to the component bundle in native shadow. Most importantly, native shadow components are faster and more performant than synthetic shadow ones.

Here's an example of an app with mixed mode components. It has a parent c-app component with c-native and c-synthetic children components. When you inspect native shadow components in the web console, they appear within a #shadow-root tag.

For the purpose of this article, parent and children components are also known as ancestor and descendant components. In contrast, a component that extends another component is referred to as a subclass and its superclass.

LWC allows inheritance, but it isn’t recommended because composition is usually more effective. Inheritance doesn’t work across namespaces, and you can’t extend the lightning namespace.

A synthetic shadow component can contain native shadow components, but the inverse isn't supported.

To enable mixed shadow mode on a component, set the static shadowSupportMode property to native.

Valid values for shadowSupportMode include:

  • any—(Deprecated) Renders the whole component subtree in native shadow DOM where possible. If the browser doesn't support shadow DOM, the subtree renders in synthetic shadow.
  • native- Renders the whole component subtree in native shadow DOM regardless of the shadowSupportMode value on the subtree components.
  • reset—Enables a subclass to opt out of receiving the shadowSupportMode value from its superclass. reset applies only if the component's superclass is using native and no parent components are using native.

Before setting shadowSupportMode to native to enable mixed shadow mode, we recommend checking that all components in the subtree are compatible with native shadow DOM. For example, you can't use the attribute selector [dir=""] in a native shadow DOM subtree because the selector only works in synthetic shadow DOM. Instead, see if your browser supports the :dir() pseudo-class in native shadow DOM. Alternatively, you can start at the leaf components and work your way up the component tree to make sure that mixed shadow mode is working as expected.

If a parent component uses native, then all components in the subtree operate in native shadow regardless of its shadowSupportMode value. Native shadow mode components can contain native shadow mode children components only.

Slots don't count as a parent-child relationship. The slotted content is owned by the slotter, not the slottee. In this example, the slotter is my-component and the slottee is c-native.

The slot renders in the same shadow mode that its parent renders in. In other words, for the purposes of determining the shadow mode, the slot is considered a "child" of the slotter, not the slottee.

Slotted content isn’t descended from the component it’s nested in, so you can slot synthetic shadow content into a native shadow component. This also means that slotted content isn’t affected by how your browser renders the #shadow-root of the component containing the slot. For example, if your browser renders a native shadow component in synthetic shadow, native content slotted into that component is still rendered in native shadow.

In the next example, <c-parent> has a native shadow subtree with the child component, <c-child> and slotted content <c-slotted>. <c-child> operates in native shadow because it’s descended from <c-parent>, but <c-slotted> can be in synthetic shadow because it’s not a descendant of <c-parent>.

On a browser that supports shadow DOM, here's how a parent and child component render in synthetic shadow when you set shadowSupportMode to native or reset.

ParentChildShadow Mode
resetnativeParent: synthetic, Child: native
-nativeParent: synthetic, Child: native

In Lightning Experience or when the @lwc/synthetic-shadow polyfill is present, reset the shadow mode to the default behavior by setting shadowSupportMode to reset.

  • If the polyfill is present, reset sets the shadow mode to synthetic shadow.
  • If the polyfill isn't present, shadowSupportMode has no impact and components render in native shadow.

To determine if an element has a synthetic shadow root, use this.template.synthetic.

In synthetic shadow, this.template.synthetic returns true. In native shadow, this.template.synthetic evaluates to undefined.

An app can include both native and synthetic shadow components. This example assumes that the app is running in Lightning Experience or the @lwc/synthetic-shadow polyfill is present. The app renders in the DOM like this.

Let's take a look at the app markup, which includes two components running in different modes.

Here's the native shadow component.

Here's the synthetic shadow component.

In Lightning Experience, synhetic shadow components render with CSS scope tokens like c-child_child or lwc-66unc5l95ad-host, depending on your custom component's apiVersion. Don't rely on these scope tokens as they are an internal implementation that can change anytime. See Synthetic Shadow.

Synthetic shadow DOM doesn't support these features that are available with native shadow.

Also, the CSS :dir() pseudo-class uses a partial polyfill in synthetic shadow DOM that only works for dir attributes explicitly on the host element.

Here are additional differences between native and synthetic shadow.

In native shadow, elements are rendered in a component's light DOM and assigned to slots in a child component's shadow DOM. Slots are created in the order that's defined in the component invoking the slotting. In synthetic shadow, LWC doesn't render elements that are passed down to child components unless they are assigned to a slot. Slots are also created in the order that's defined in the target slottable component.

For a component c-parent that contains c-child, c-child has a span that's not assigned to a slot. In native shadow, this span is rendered in the DOM. However, in synthetic shadow, the span doesn't exist in the DOM.

To determine which slot an element is assigned to, call the assignedSlot API. Previously, when the synthetic shadow polyfill is loaded, assignedSlot returned null on elements assigned to slots in native shadow components. Now, the API returns the <slot> element for slotted elements in native shadow components.

To return an array of all elements at the specified coordinates, use the elementsFromPoint API, including for synthetic shadow DOM elements that aren’t visible in the DOM. In the following example, <c-inner> is invisible (zero width and height), but the <div> that it contains is visible.

In synthetic shadow, calling outer.shadowRoot.elementsFromPoint() returns [<div>, <c-outer>, <html>]. In native shadow, it returns [<c-inner>, <c-outer>, <html>].

With synthetic shadow, apps rely on global stylesheets to apply styles throughout the DOM. The same isn't possible in native shadow, where a component's styles have to be added to the component bundle.

However, with synthetic shadow, a shared stylesheet at the top level of your document can style all components on a page. To allow global or shared styles to apply to child components, use @import to include the shared stylesheet in a component that requires it.

Importing the same stylesheet in multiple components does not impact performance as LWC handles the deduplication of CSS for you.

While the CSS module import works for most shared CSS libraries, it doesn't work if the CSS contains selectors that traverse shadow boundaries. Let’s say you have a parent and child component with this CSS.

This CSS doesn't work if the .parent and .child selectors are in separate components. In this case, use CSS custom properties or another technique to conditionally render children based on their parent.

For example, you can pass the color property to the child component.

The color that displays in the child component is determined by the parent component.

Alternatively, consider migrating your components to use light DOM instead if you don't require encapsulation on your components.

With synthetic shadow, you can create references across shadow boundaries by dynamically setting attributes in both the parent and child components. With native shadow, you can't do the same as element IDs are scoped to a specific component.

We recommend the following options.

  • If an element references another element's ID, place both in the same component.
  • If you use aria-labelledby, duplicate the strings across elements using aria-label.

Alternatively, if you're working with components that don't require encapsulation, consider using light DOM. For example, you have two light DOM components as siblings within the same shadow DOM parent component, and the IDs are shared between the siblings.

Base components are currently not supported for mixed shadow mode. Salesforce is preparing the base components for native shadow DOM following Web Components standards. The internal structure of base components continue to change as we work on enabling future support of mixed shadow mode and native shadow DOM. Currently, styling on base components can break when they are placed within a native shadow component; native shadow renders all child components without synthetic shadow.

With synthetic shadow, listeners can handle non-composed events outside of the root LWC node if the event originates from a non-LWC component in the subtree. However, shadow DOM doesn't support this behavior.

Also, blur and focus events don't bubble or compose correctly in synthetic shadow. Let's say you have a focusable element in a component.

In synthetic shadow, blur and focus events don't bubble up.

The same works as expected in native shadow.

In synthetic shadow, form elements like <input> and <button> are associated with the enclosing <form> tag, maintaining form behavior like validity states and form submission. However, the same isn't true for native shadow. Form-Associated Custom Elements isn't currently supported.

With synthetic shadow, slotted elements are rendered if they are assigned to a slot only. For slotted elements that are never assigned to a slot, their lifecycle hooks are never invoked.

Furthermore, lifecycle hooks in synthetic shadow are invoked in the order of appearance after they are assigned. Contrastingly, in native shadow, they are invoked in the order of appearance in the template.

In WebDriver-based tests, XPath selectors ignore synthetic shadow DOM, but respect native shadow DOM. This means that your XPath selectors may not work in native shadow DOM. You should use UTAM or kagekiri instead of XPath selectors.

In synthetic shadow where light DOM nodes surround a shadow node, the innerText property doesn't hide shadow content, but it hides shadow content in native shadow. Other Web APIs like innerHTML, outerHTML, and textContent also display similar behavior.

Let's look at a c-light light DOM component that contains a c-shadow shadow DOM component.

In synthetic shadow, calling innerHTML from c-light returns <c-shadow><h1>Hello Shadow DOM</h1></c-shadow>. In native shadow, calling innerHTML from c-light light DOM component returns <c-shadow></c-shadow>.