Build Components in Mixed Shadow Mode (Developer Preview)

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

Mixed shadow mode is available as a developer preview. This feature isn't generally available unless or until Salesforce announces its general availability in documentation or in press releases or public statements. All commands, parameters, and other features are subject to change or deprecation at any time, with or without notice. Don't implement functionality developed with these commands or tools.

All major browsers now support shadow DOM. Salesforce maintains the synthetic shadow polyfill for legacy browsers such as older versions of Microsoft Edge.

To simplify development and testing, the polyfill is currently used even on browsers that support shadow DOM. 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.

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. Also some shadow DOM features, such as ::part, aren't supported in synthetic 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. Synthetic shadow roots aren’t visible in developer tools, but they’re shown in the following examples for illustrative purposes.

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.

Synthetic components can contain native components, but the inverse isn't supported.

Mixed shadow mode isn't available in your org by default. Contact Salesforce to participate in the developer preview.

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

Valid values for shadowSupportMode include:

  • any—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.
  • 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 any and no parent components are using any.

Before setting shadowSupportMode to any 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 any, 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. This limitation is a side effect of the shadowSupportMode property being applied to the entire subtree.

By default, slotted content is rendered in native shadow. 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 component in synthetic shadow, native content slotted into that component is still rendered in native shadow.

Below, <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 when you include the @lwc/synthetic-shadow polyfill and set shadowSupportMode to any or reset.

ParentChildShadow Mode
resetanyParent: synthetic, Child: native
-anyParent: synthetic, Child: native

When you include the @lwc/synthetic-shadow polyfill, 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.

An app can include both native and synthetic shadow components. This example assumes that the app is using the @lwc/synthetic-shadow polyfill.

The app includes two components running in different modes.

Here's the native component.

Here's the synthetic component.

Here we discuss some differences between native and synthetic shadow.

In native shadow DOM, elements are rendered in a component's light DOM and assigned to slots in a child component's shadow DOM. But in synthetic shadow DOM, LWC doesn't render elements that are passed down to child components unless they are assigned to a slot.

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 prevent styles at the top level from getting applied 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.

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.

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.

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, base components might not display the correct styling when placed within a native shadow component; native shadow renders all child components without synthetic shadow.

Our benchmarks have shown that native shadow components are, in some scenarios, up to 50% faster than their synthetic counterparts. Removing the synthetic shadow polyfill would also remove 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.

Synthetic shadow doesn't support some shadow DOM features, such as ::part.

Consider these additional differences between native shadow and synthetic shadow.

Synthetic ShadowNative Shadow
Slots are created in the order that's defined in the target slottable component.Slots are created in the order that's defined in the component invoking the slotting.
The innerText property doesn't hide shadow content.The innerText property hides shadow content.
this.template.firstChild returns the first element in the template.this.template.firstChild returns a <style> element in native shadow in some browsers.