Since its inception, the Lightning Web Components (LWC) framework has leveraged shadow DOM, a browser standard that provides encapsulation and composability for web components. To make shadow DOM work even better in LWC, we are now allowing access to the native capabilities provided by modern browsers.
For component authors, this change has the potential to improve the performance, capability, and compatibility of your LWC components. However, there may be some steps required to ensure that your components are ready for the new, native shadow DOM that we are introducing in Spring ’24.
Our plan to embrace native shadow DOM
To set the stage for the rest of this article, we’ll first give you a brief history lesson on why shadow DOM works the way it does in LWC.
The history of shadow DOM and LWC
As one of the first major web component frameworks, LWC was a pioneer in its use of shadow DOM. The rest of the web has been slowly catching up, and shadow DOM usage has been growing over the years. Many newer frameworks, such as Lit, Stencil, and FAST, have also embraced shadow DOM. But by being an early adopter, LWC took on several challenges.
When LWC debuted back in 2017, shadow DOM had poor support across Salesforce-supported browsers. To support legacy browsers, such as Internet Explorer 11, a synthetic (or polyfill) version of shadow DOM was needed. However, shadow DOM is not a simple feature that can be transparently polyfilled — it affects multiple aspects of the web platform, and some of its features are simply impossible to emulate.
Because of this, the LWC team created a partial polyfill, called synthetic shadow DOM. This polyfill covers most, but not all, of the shadow DOM standard. The differences between the two are small, but still significant enough that it would have been a burden on component authors to support different behavior for different browsers. To avoid this compatibility headache, LWC chose to serve the polyfill to all browsers, providing a consistent experience across the board.
Today, the situation is much different. With the end of support for IE11, all of the supported browsers in Lightning Experience now support shadow DOM. Strictly speaking, the polyfill is no longer necessary.
At the same time, browser vendors are constantly shipping new shadow DOM features, such as parts, constructible stylesheets, and imperative slots. These features would be difficult or impossible to emulate in the polyfill. Furthermore, our benchmarks have consistently shown that native shadow DOM is faster than synthetic shadow DOM, both in terms of JavaScript and CSS.
So, it’s time to sunset synthetic shadow DOM. But we know that not all components today will support native shadow DOM out of the box due to the minor inconsistencies mentioned above. So, how do we safely move the ecosystem to the more performant, standards-based solution?
Introducing shadow DOM mixed mode
With shadow DOM mixed mode, component authors can opt into native shadow DOM on a component-by-component basis. Use mixed shadow mode for a gradual migration rather than a disruptive all-or-nothing change.
To opt your component into native shadow DOM, use the shadowSupportMode
static property.
Previously, the mixed shadow mode developer preview used the 'any'
value. shadowSupportMode = 'any'
is deprecated and superseded by shadowSupportMode = 'native'
.
This component now runs using native shadow DOM mode. If you test your component and it continues to work fine with this change, then you’re all done! However, if you run into any errors, you’ll need to modify your component to properly support native shadow DOM.
Note that you may also run your Jest tests in native shadow DOM mode. However, this is not necessarily a guarantee that the components will work in practice since some issues (such as styling) are difficult or impossible to test in a headless environment like Jest.
Descendant components are forced into native mode
For the most part, the goal of shadow DOM mixed mode is to allow synthetic and native components to live side by side. However, in some cases, this is not possible.
In short, this is okay:
But this is not okay:
In other words, you cannot have a synthetic shadow component inside of a native shadow component.
Because of this, LWC will automatically force any shadow DOM-using components that are inside of a native shadow component into native mode. This includes children, grandchildren, etc. – all descendants.
This points to a good migration strategy: start with your leaf nodes. If you migrate your leaf nodes (i.e., your components without children) to native shadow first, then you will probably have a less rocky migration path.
Note that slots do not count as a parent-child relationship. The slotted content is owned by the component putting content into the slot, not the component that declares the <slot>
. So for instance, this is valid:
In this case, the <x-synthetic>
component will not be opted into native mode (unless some other ancestor is native).
Also, note that light DOM components are unaffected by this restriction. Light DOM components are compatible with both native shadow and synthetic shadow components.
The impact of native shadow DOM migration
Below, we’ll cover some of the more common differences between synthetic and native shadow DOM that may affect the migration process.
Not all base Lightning components support native shadow DOM
As you are migrating your components to native shadow DOM, it’s important to understand that not all base components are ready to support shadow DOM. We are on this journey with you, and our own components will need time to make the transition.
As such, you should be careful when migrating components that contain references to base Lightning components. For example:
<lightning-button>
is a base Lightning component. In Spring ’24, base Lightning components don’t yet support native shadow DOM. If you attempt to migrate your own component to native shadow DOM, this will force components like <lightning-button>
into native mode, and you may see incorrect styling or functional bugs.
As base Lightning components roll out with explicit support for native shadow DOM, we will announce the new capabilities in the release notes.
The rest of this guide assumes that your component tree contains only components you own.
Global styling
One of the biggest differences between native and synthetic shadow is with CSS. In synthetic shadow, styles cannot leak out, but they can leak in. Whereas in native shadow, styles cannot leak in either direction.
Synthetic shadow | Native shadow | |
Styles leak out? | ✅ No | ✅ No |
Styles leak in? | ❌ Yes | ✅ No |
A common scenario that may cause problems is a global stylesheet at the top level of the page — for instance, an external .css
file loaded as a static resource.
In the DOM, the CSS may look like this:
In synthetic shadow, this global stylesheet can style all components on the page. In native shadow, those components are encapsulated and thus unaffected by the CSS.
Note: There are exceptions to this rule for inherited properties, such as color
, font-family
, and custom properties. But as a mental model, it’s largely true that shadow DOM restricts all styles from leaking in or out.
To fix this, use @import
from your component’s *.css
file to include the shared stylesheet in any component that needs it.
Note: It’s not a performance concern to duplicate this shared CSS across components. Browsers are smart about deduplicating CSS under the hood, and LWC uses other tricks to speed this up.
Keep in mind that the above technique will work for many off-the-shelf CSS libraries, but not all of them. For instance, it will not work if the CSS contains selectors that cross shadow boundaries. For example:
The above CSS will not work if the .parent
and .child
elements live in separate components. Instead, you can use CSS custom properties, property passing, or another technique to conditionally style children based on their parents.
If the global CSS in question is SLDS (as used in Lightning Experience), then the recommendation is to migrate from slds-*
classes to SLDS styling hooks. This is the officially-supported design system built with native shadow DOM in mind.
Another solution is to migrate your shadow DOM components to use light DOM instead. However, this has implications for security and encapsulation, so be careful when making this choice.
IDs and accessibility
Attributes that express a semantic relationship between two DOM elements, such as aria-labelledby
and for
, do not work when the elements are in separate native shadow DOM trees.
In other words, this will not work:
This is because in native shadow DOM, element IDs are scoped to a particular component.
Synthetic shadow DOM emulates some, but not all, of this behavior. Because of this, some developers have used workarounds that only work in synthetic shadow DOM, and which allow references across shadow roots.
In native shadow DOM, these workarounds do not work and we don’t recommend them. Instead, we recommend to either:
- Use light DOM for components that need to have a semantic relationship between two components.
- Use attributes that don’t require references to other components, e.g.,
aria-label
instead ofaria-labelledby
. - Use an ARIA live region to communicate to screen readers if none of the above options work for you.
In the future, we hope that there will be better solutions to the problem of cross-root ARIA references. At Salesforce, we are heavily involved in the W3C Accessibility Object Model working group, working on proposals such as Cross-Root ARIA to improve the experience of building accessible web components. Stay tuned for updates as the relevant browser standards evolve!
Other differences
There are some other small differences between native shadow and synthetic shadow, which are unlikely to affect you during your migration. But just for the sake of completeness, here they are:
- In synthetic shadow DOM, slots are “lazy” and are created in the order defined in the target slottable component. In native shadow DOM, slots are “eager” and are created in the order defined in the component doing the slotting.
- The
innerText
property does not always hide shadow content in synthetic shadow, but it does in native shadow. This also applies toinnerHTML
,outerHTML
, andtextContent
. - 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 if you are relying on the synthetic shadow behavior. You should use UTAM or kagekiri instead of XPath selectors.
- In synthetic shadow DOM, form elements, such as
<input>
and<button>
, are automatically associated with an enclosing<form>
. In native shadow DOM, they are not. In a future release, we plan to add support for Form-Associated Custom Elements to help resolve this limitation. - In synthetic shadow DOM,
blur
andfocus
events do not bubble/compose correctly, but in native shadow DOM, they do. If you are relying on the broken behavior of synthetic shadow, you may be surprised when your component starts receiving these events in native shadow. - The CSS
:dir()
pseudo-class uses a partial polyfill in synthetic shadow DOM that only works fordir
attributes explicitly on the host element. In native shadow DOM, the native browser:dir()
is used instead, which works consistently regardless of whether thedir
is on the host element or not.
Native shadow DOM also supports several features that synthetic shadow DOM does not, including:
- Server-Side Rendering in LWR
- The CSS
::part
pseudo-element - Imperative slots
- No need to use
lwc:dom="manual"
– you can manipulate any DOM node without a special directive
This is only a partial list. There are many other differences between native and synthetic shadow DOM.
Checking if a component is native or synthetic
In some cases, you may want to programmatically check if a component is running in native mode or synthetic mode. You can do so by evaluating this.template.synthetic
inside your component.
You can do this anywhere inside of a component where you have access to this.template
. Note that this only works for the current component context — you cannot inspect child components this way.
In native shadow, this.template.synthetic
will evaluate to undefined
. In synthetic shadow, it will be true
.
Conclusion
Migrating from synthetic to native shadow DOM should hopefully be a painless process for most of your components. In many cases, the change should be as simple as adding static shadowSupportMode = 'native'
.
This migration might not always be easy. But in the end, adopting native shadow DOM will help bring your LWC components into closer alignment with web standards, new browser features, and improved performance. Native shadow DOM helps unlock the original vision of LWC as a standards-based framework that leans into native browser capabilities and embraces the web platform.
Native shadow DOM is a journey that all LWC users are on, and we are on it with you. So if you run into any issues that aren’t covered here, please drop us a note on IdeaExchange or GitHub, or document your solution on StackExchange.
Resources
About the authors
Nolan Lawson is a Principal Member of Technical Staff on the Lightning Web Components framework team, where he focuses on performance, accessibility, and web standards.
Diana Widjaja is a Lead Technical Writer who has been writing for Lightning Web Components, Base Lightning Components, Lightning Data Service, and many other teams.