SfSkills Domains Docs GitHub

Home / lwc / lwc-shadow-vs-light-dom-decision

lwc-shadow-vs-light-dom-decision

Decide Shadow DOM vs Light DOM for an LWC — Salesforce distribution rules, Locker/LWS, third-party library needs. Triggers: shadow vs light DOM, renderMode decision. NOT for implementing Light DOM after the decision — use lwc/lwc-light-dom.

Open the skill folder on GitHub


LWC Shadow vs Light DOM Decision

Activate this skill when a developer is choosing the render mode for an LWC and the decision is non-trivial — a corporate CSS theme has to reach inside, an Experience Cloud LWR site needs SEO and screen-reader visibility, an Aura host page is bleeding styles in, or a security-sensitive form needs the strongest possible encapsulation. The output is a single recommendation (Shadow DOM default OR Light DOM opt-in), the CSS / event / accessibility strategy that goes with it, and a checker that flags the most common drift between intent and implementation.

This is NOT a generic Light DOM how-to (see lwc/lwc-light-dom for the mechanics of opting in). It is NOT a CSS styling reference (see lwc/lwc-styling-and-slds for SLDS Blueprint compliance). And the managed-package case is a hard constraint, not a decision: components distributed through managed packages MUST stay Shadow DOM — Salesforce explicitly forbids Light DOM for managed-package distribution because the styles would leak into every consumer org.


Before Starting

Gather this context before recommending a render mode:


Core Concepts

1. Encapsulation: Shadow DOM is a strong boundary, Light DOM is not

Shadow DOM (the LWC default) creates a separate tree under a shadow root. Host-page CSS does not bleed in (body { font-family: ... } does not affect the component's text), document.querySelector from outside cannot see internal nodes, and ARIA references from the host page cannot point at internal IDs. The boundary is the encapsulation guarantee.

Light DOM, opted into with static renderMode = 'light', renders the component's template directly into the host element with no shadow root. Host-page styles bleed in (intentionally — that is usually the whole reason you chose Light DOM), document.querySelector from outside finds the nodes, and accessibility tooling sees a continuous tree.

The decision is whether you want the boundary or not. Most internal-app components want it. Components that have to integrate with a corporate CSS framework or be SEO-indexed on an LWR site usually do not.

2. CSS scoping: :host, *.scoped.css, and --slds-* design tokens

In Shadow DOM, the component's <componentName>.css file is automatically scoped — its selectors apply only inside the shadow root. The :host selector targets the component's host element from inside its own stylesheet. Parent CSS does not cross the boundary, but CSS custom properties (design tokens like --slds-c-button-color-background) DO inherit through the boundary — that is the canonical way to let a parent theme a child while keeping encapsulation.

In Light DOM, the component's <componentName>.css is global by default — every selector in it applies across the whole page. To keep CSS scoped to the component, rename the file to <componentName>.scoped.css. The :host selector does not work in Light DOM (there is no host shadow root) and Salesforce silently drops the rule.

3. Events: composed: true matters under Shadow DOM, is redundant under Light DOM

Custom events dispatched from inside a Shadow DOM tree need composed: true to cross the shadow boundary and reach a listener outside the component. The default is composed: false, which means the event stops at the boundary.

Under Light DOM there is no boundary, so composed: true is the default behavior — explicitly setting composed: false on a Light DOM component is dead configuration that confuses readers.

bubbles: true is independent of render mode and still matters in both.

4. Slots: scoped (Shadow) vs positional (Light)

In Shadow DOM, slotted content is distributed but not moved — the slotted children stay in the host's light tree (from their perspective), and the component's CSS cannot directly style them with simple selectors. Use ::slotted() or design tokens.

In Light DOM, slotted content is inlined into the rendered tree positionally — the slotted children become real children of the wrapping element, and the component's stylesheet (if global) can style them directly. This is closer to how plain HTML composition works but means lifecycle hooks fire in a different order than under Shadow DOM.

5. lwc:dom="manual" is NOT the same as Light DOM

lwc:dom="manual" is a per-element opt-out of LWC's diffing for that one node — useful when a third-party library (a chart, a rich-text editor) needs to mutate a specific DOM region. The component is still Shadow DOM (or still Light DOM); only that one element is excluded from LWC's reconciliation.

Light DOM is a whole-component decision via static renderMode = 'light'. Reaching for lwc:dom="manual" to "let a third-party library work" usually means you should instead audit whether the component genuinely needs Light DOM, or whether the library can target a single lwc:dom="manual" node inside a Shadow DOM component.


Common Patterns

Pattern A — Shadow DOM (default) with --slds-* design tokens for theming

When to use: the component lives in internal LEX or in a flow screen. SLDS is the only CSS that has to reach in. A parent component or a global SLDS Styling Hooks override needs to theme it.

How it works: keep static renderMode unset (Shadow DOM is the default). In <componentName>.css, theme via SLDS Styling Hooks:

:host {
    --slds-c-button-color-background: var(--brand-primary, #0070d2);
}

Custom CSS properties pierce the shadow boundary, so a parent setting --brand-primary will reach in. Selectors like body { ... } from the host page will not — that is the encapsulation you want.

Why not Light DOM: Light DOM gives up the encapsulation that made the component safe to drop into any LEX page in the first place. If the only requirement is theming, design tokens already solve that without sacrificing the boundary.

Pattern B — Light DOM for a Bootstrap / corporate-theme LWR site

When to use: the component runs in an Experience Cloud LWR site that uses Bootstrap (or a custom corporate theme published as a static resource). The site must look consistent with the rest of the corp web property, and rewriting every component in SLDS-only is not on the table.

How it works: opt in to Light DOM and let the host page's CSS reach in:

import { LightningElement } from 'lwc';
export default class CorpHeader extends LightningElement {
    static renderMode = 'light';
}

Use <componentName>.scoped.css for any styles that must stay component-local. Drop the :host selector entirely (it does not work in Light DOM). Custom events no longer need composed: true.

Why not Shadow DOM: Bootstrap utility classes (.col-6, .btn-primary, .text-muted) bind to selectors at the document root. They cannot cross a shadow boundary, and Salesforce does not expose Bootstrap as design tokens. The boundary is the blocker.

Pattern C — Hybrid: Shadow DOM components inside an Aura host that uses a corporate theme

When to use: the org uses Aura as its top-level host (Aura sites, Aura record pages, an Aura console app) and a corporate theme is loaded at the Aura level. The LWC components are individually fine in isolation but the host's styles need to influence them.

How it works: keep each LWC as Shadow DOM (so each is still individually safe for managed-package candidacy and still has its boundary), and propagate the host's theme via design tokens. Either:

  1. Set the tokens on :host in the outermost LWC and have nested LWCs inherit, or
  2. Have the Aura wrapper set CSS custom properties on the LWC's host element directly via style="--brand-primary: #...;".

Why not Light DOM everywhere: the Aura → LWC → LWC chain stays cleaner with one CSS contract (design tokens) than with three different scoping rules. Reach for Light DOM only when a specific component cannot solve its problem with tokens.


Decision Guidance

The main artifact of this skill. Run the user's component through this table:

SituationRecommended ModeCSS StrategyReason
Corporate CSS theme (Bootstrap / Tailwind / corp utility classes) requiredLight DOM (static renderMode = 'light')Global stylesheet; component-local styles in *.scoped.cssUtility-class frameworks bind at the document root; they cannot pierce a shadow boundary. Design tokens are not a substitute for hundreds of utility classes.
Aura host integration where the Aura page loads a global stylesheet that must reach insideLight DOM (often) — or Shadow DOM with design tokensGlobal if Light DOM; :host + --* tokens if ShadowAura host CSS does not pierce shadow boundaries. If the Aura theme is a small token set, Shadow + tokens is cleaner. If the Aura theme is hundreds of selectors, Light DOM is the pragmatic answer.
Accessibility-critical form (screen-reader navigation, ARIA cross-references between sibling regions)Light DOM if external ARIA references must point in; Shadow DOM otherwise*.scoped.css if Light; design tokens if Shadowaria-describedby and aria-labelledby resolve only within the same root. Cross-shadow ARIA references silently fail. If the form is self-contained, Shadow DOM is fine and the screen reader navigates the shadow tree natively.
Data table with horizontal overflow / sticky headers / virtualized rowsShadow DOM (default):host + design tokens; consider lwc:dom="manual" for the virtualized regionThe table's internal layout is the component's concern, not the host's. Encapsulation prevents host-page CSS from breaking row heights or overflow rules.
Modal with backdrop / overlay / focus trapShadow DOM (default), or use lightning-modal:host + design tokens; portal via lightning-modal for true page-level layeringBackdrops and focus traps need predictable z-index and positioning that host CSS cannot stomp on. Reach for lightning-modal (which handles the portal) before hand-rolling.
Experience Cloud LWR site (SEO-indexed, public-facing, theme-driven)Light DOM (commonly)Global stylesheet; SLDS or corporate themeCrawlers do not index inside shadow roots reliably. LWR theming usually depends on global styles reaching inside. This is the canonical Light DOM use case.
Security-sensitive form (PII, payment, credentials) NOT going to a managed packageShadow DOM (default):host + design tokensDOM-level isolation reduces the surface for accidental host-page CSS or JS from interfering. LWS still applies to both modes, but Shadow DOM adds defense in depth.
Component will be distributed in a managed packageShadow DOM (mandatory):host + design tokensSalesforce explicitly forbids Light DOM in managed packages because the global styles would leak into every consumer org. Not a decision — a constraint.
Internal LEX-only utility component (no external CSS, no SEO)Shadow DOM (default)Component CSS is scoped automatically; theme with --slds-* tokensPay no cost for encapsulation you do not need to break.

  1. Identify the runtime. Determine where the component will render (internal LEX / LWR site / Aura site / managed package / flow screen). If managed package, stop — Shadow DOM is mandatory.
  2. Identify the CSS contract. SLDS only, SLDS + corp theme via design tokens, third-party utility framework, or all-custom. Map each to a row in the Decision Guidance table.
  3. Identify ARIA cross-references and sr-only content. Walk every aria-describedby / aria-labelledby / aria-controls / aria-owns and confirm both sides live in the same root under your chosen mode. Cross-shadow references silently break.
  4. Pick the mode and document the blocker. Write down the single sentence "we are Light DOM because <Bootstrap utility classes / corp theme / LWR SEO>" or "we are Shadow DOM because <default + design tokens are sufficient>". The blocker statement is the future-proofing.
  5. Wire the CSS strategy. Shadow DOM → component CSS automatically scoped, theme via --slds-* tokens on :host. Light DOM → component CSS is global, rename to *.scoped.css for component-local styles, drop any :host rules.
  6. Wire the event strategy. Shadow DOM → set composed: true on every custom event that has external listeners. Light DOM → omit composed, default is fine; do NOT set composed: false (it is dead config).
  7. Run scripts/check_lwc_shadow_vs_light_dom_decision.py against the LWC bundle directory to flag the four common drifts (Light DOM with :host, Light DOM with composed: false, Shadow DOM with global selectors, missing composed: true on events that look external).

Review Checklist


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. :host is silently dropped in Light DOM — no compile error, no runtime warning, the rule just does nothing. Easy to miss in code review when a component was migrated from Shadow to Light without sweeping the CSS.
  2. aria-describedby does not cross shadow roots — a screen reader will read the source element but skip the description because the target ID is not in the same root. The form looks accessible in DevTools and fails accessibility testing.
  3. Custom events without composed: true look like they work in Storybook / jest — both run in a single tree where the boundary is irrelevant. The bug only surfaces in LEX where a parent listener is in a different shadow root.
  4. Light DOM CSS is global by default — naming corpHeader.css in a Light DOM component leaks every selector to the entire page. The *.scoped.css rename is the only fix; there is no runtime opt-in.
  5. Slotted content lifecycle differs — under Shadow DOM, connectedCallback fires on the slotted child before slot assignment; under Light DOM, the slotted children are inlined and lifecycle order can differ. Logic that assumes "I have my slotted children at connectedCallback time" should use slotchange or renderedCallback regardless.
  6. Managed packages forbid Light DOM — discovered at packaging time, not at write time. If the component is destined for the AppExchange, the answer is Shadow DOM regardless of every other factor.

Output Artifacts

ArtifactDescription
Render-mode decisionOne sentence: "Shadow DOM (default), themed via --slds-* tokens" OR "Light DOM, blocker: <Bootstrap / corp theme / LWR SEO>"
CSS strategyWhich file lives in <componentName>.css vs <componentName>.scoped.css; whether :host is present (Shadow only); design tokens used
Event-composition mapList of dispatched custom events with composed / bubbles settings
Accessibility planList of ARIA cross-references and confirmation that source + target live in the same root
Checker runscripts/check_lwc_shadow_vs_light_dom_decision.py clean exit

Package files