Shadow DOM Basics
Encapsulate markup and styles with shadow roots — open vs closed, slots, CSS boundaries, and events retargeting.
- javascript
- shadow-dom
Global CSS and querySelector see the whole document — until Shadow DOM creates a scoped subtree. Web components use it so a widget’s internals don’t clash with the page, and page CSS doesn’t accidentally restyle the widget’s guts.
class FancyButton extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
button {
background: rebeccapurple;
color: white;
border: 0;
padding: 0.5rem 1rem;
}
</style>
<button part="control"><slot></slot></button>
`;
}
}
customElements.define('fancy-button', FancyButton);
<fancy-button>Save</fancy-button>
open vs closed
| Mode | element.shadowRoot |
Use |
|---|---|---|
'open' |
accessible | default for most apps |
'closed' |
null from outside |
weak hiding; not real security |
Closed mode stops casual access; determined code can still keep the reference from attachShadow’s return value. Not a security boundary.
Style encapsulation
- Styles inside the shadow root apply to the shadow tree.
- Page selectors don’t pierce in (except documented escape hatches).
:hoststyles the host element from inside.::slotted()styles distributed light-DOM children (limited).::part()+partattributes let pages theme specific internals deliberately.
/* page CSS */
fancy-button::part(control) {
border-radius: 8px;
}
CSS variables inherit into shadow trees — primary theming channel:
fancy-button {
--fb-bg: navy;
}
/* inside shadow */
button { background: var(--fb-bg, rebeccapurple); }
Slots (light DOM projection)
<!-- in shadow -->
<slot name="icon"></slot>
<slot></slot> <!-- default -->
<!-- usage -->
<fancy-button>
<span slot="icon">★</span>
Save
</fancy-button>
Slotted nodes stay in the light DOM (children of the host) for accessibility and SEO; they’re rendered at slot positions.
Events
Events retarget so external listeners see the host as the target when the real target is inside the shadow tree (for non-composed details). Use { composed: true } on CustomEvent if you need to cross the boundary.
this.dispatchEvent(new CustomEvent('save', { bubbles: true, composed: true }));
vs iframe
Shadow DOM shares the JS realm and is lighter; iframes are hard origin isolation. Pick shadow for widgets; iframes for untrusted third-party documents.
Interview answer (out loud)
“Shadow DOM attaches a scoped tree to an element so internal markup and styles don’t leak either way by default. open mode exposes shadowRoot; slots project light DOM children; CSS variables and part enable controlled theming. Events may retarget; composed events cross the boundary.”
Form elements and shadow
Native form association across shadow roots has improved (form attribute, ElementInternals). If you build custom inputs, read up on form-associated custom elements — shadow encapsulation otherwise breaks FormData expectations.
Focus and tabs
Shadow trees participate in focus; delegatesFocus option on attachShadow helps hosts forward focus into internals.
this.attachShadow({ mode: 'open', delegatesFocus: true });
Querying
host.shadowRoot.querySelector('.inner');
// document.querySelector('.inner') won't see it
Tests must pierce shadow roots intentionally (shadowRoot) or use user-facing roles via Testing Library, which is usually better.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.