Choosing Encapsulation in Stencil.js: Shadow, Scoped, or Light DOM – A Decision Guide
Decide how your Stencil.js component should encapsulate styles—shadow DOM, scoped styles, or light DOM. Compare trade‑offs, see a concrete my‑button example, and learn when each mode fits your design‑system needs.
19 Apr 2026, 12:03 UTC

Why Encapsulation Matters in Stencil.js
When building reusable UI pieces, you must decide how the component’s styles interact with the rest of the page. Stencil offers three built‑in modes: shadow: true (native Shadow DOM), scoped: true (Stencil‑generated light‑DOM scoping), and the default light DOM with no flag. Each mode changes the contract you expose to consumers, the theming approach you can use, and how slots behave.
Decision Framing
The decision can be boiled down to three questions:
- Do you need full style isolation for the component?
- Will downstream teams override styles or rely on global resets?
- Do you need to expose parts for fine‑grained theming?
Answering these will point you to the appropriate encapsulation mode.
Encapsulation Modes in a Nutshell
| Mode | Isolation | Style Override | Slot Support | Part Selector |
|---|---|---|---|---|
| shadow: true | Strongest – native Shadow DOM | Only via CSS variables or ::part() | Native – <slot> works as expected | Supported – part="…" |
| scoped: true | Moderate – Stencil rewrites selectors | Global CSS can override (class names) | Emulated – works in most recent Stencil releases | Not supported – ::part() unavailable |
| light DOM (default) | Weakest – no encapsulation | Full global CSS overrides | Native – <slot> works | Not supported |
Trade‑Off Summary
Below is a compact table that maps each mode to the key trade‑offs you’ll encounter in real projects.
| Aspect | Shadow DOM | Scoped Styles | Light DOM |
|---|---|---|---|
| Style Isolation | ✔︎ | ✖︎ (Scoped) | ✖︎ |
| Consumer Styling Flexibility | ✖︎ (Limited) | ✔︎ (Class overrides) | ✔︎ (Full CSS) |
| Theming via CSS Variables | ✔︎ | ✔︎ | ✔︎ |
| Part Selector for Theming | ✔︎ | ✖︎ | ✖︎ |
| Third‑Party CSS Reach | ✖︎ | ✔︎ (via scoping) | ✔︎ |
| Migration Effort (Shadow ↔ Scoped) | High – breaking change | High – requires re‑build | Low – no encapsulation changes |
Concrete Example – A Design‑System Button
Below is a minimal my-button component that demonstrates how to expose a theming contract using Shadow DOM and CSS custom properties. The same component can be re‑compiled with scoped: true or no flag to see the differences.
// src/components/my-button/my-button.tsx
import { Component, Prop, h } from '@stencil/core';
@Component({
tag: 'my-button',
shadow: true, // ← change to scoped: true or remove for light DOM
styleUrl: 'my-button.css',
scoped: false
})
export class MyButton {
@Prop() variant: 'primary' | 'ghost' = 'primary';
render() {
return (
<button part="button" class={this.variant}>
<slot />
</button>
);
}
}
/* src/components/my-button/my-button.css */
:host {
display: inline-block;
}
button {
background: var(--my-button-bg, #0069d9);
color: var(--my-button-color, #fff);
border: none;
padding: 0.5rem 1rem;
font: inherit;
cursor: pointer;
}
button.ghost {
background: transparent;
color: var(--my-button-ghost-color, #0069d9);
border: 1px solid var(--my-button-ghost-color, #0069d9);
}
Using the Component in a Page
Place the component in a demo page and apply global styles to verify encapsulation.
<!-- demo.html – run with a local server or Stencil dev server -->
<style>
button { background: red; }
my-button::part(button) { border-radius: 8px; }
.theme-wrapper { --my-button-bg: #28a745; }
</style>
<my-button>Default</my-button>
<my-button variant="ghost">Ghost</my-button>
<div class="theme-wrapper">
<my-button>Themed</my-button>
</div>
Validation Checklist
- Open the page in a browser and inspect the Shadow DOM: you should see a
#shadow-rootcontaining thebuttonelement. - Change the global
button { background: red; }rule and verify that the internal button background remains unchanged. - Modify
--my-button-bgon the wrapper element and confirm the button’s background updates accordingly. - Apply
my-button::part(button)rules from the page stylesheet and ensure the border‑radius style is applied.
When to Pick Each Mode
- Shadow DOM (recommended for public component libraries) – Use when you want the component to be a true encapsulated web component. Ideal for design‑system pieces that will be dropped into arbitrary pages. Theming is done exclusively with CSS variables and
::part()selectors. - Scoped Styles (useful for internal apps with a global stylesheet) – Choose when you need the component to live in the page’s CSS cascade but still want to avoid selector collisions. This mode is also handy when third‑party CSS (e.g., a CSS framework) must style internal elements.
- Light DOM (plain) – Opt for when the component is part of a monolithic app that already manages a single global stylesheet. It offers the most flexibility for downstream teams but requires disciplined naming to avoid clashes.
Practical Tips for Migration
- Changing from
shadow: truetoscoped: true(or vice versa) is a breaking change because the CSS selector structure changes. Always bump the component’s version and document the new contract. - When switching to Shadow DOM, expose all interactive parts via the
partattribute if you want consumers to style them. Otherwise, rely solely on CSS variables. - Keep the same CSS variable names across modes to maintain a consistent theming API.
Conclusion
Choosing the right encapsulation mode in Stencil.js is a design‑system decision that balances isolation, theming flexibility, and consumer control. Use Shadow DOM for maximum encapsulation and a clean theming contract, scoped styles when you need global CSS to reach component internals, and light DOM for maximum flexibility in tightly coupled applications. The example above demonstrates how to expose a design‑system button that works consistently across all three modes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.