Shadow DOM
Also Known As
TL;DR
Explanation
Shadow DOM is one of the three foundational pillars of Web Components (alongside Custom Elements and HTML Templates). It lets you attach a 'shadow root' to a host element with Element.attachShadow({ mode: 'open' | 'closed' }), creating a separate DOM subtree whose CSS rules, IDs, and JavaScript queries are scoped to that root. Selectors like document.querySelector('#foo') do not match elements inside a shadow tree, and page-level CSS cannot accidentally restyle shadow content. This is how native elements such as <video> and <input type="range"> keep their internal structure hidden from the page. The boundary is crossed explicitly — via slots for projected content, CSS custom properties and ::part() for theming, and the host's exposed API for JS. 'Open' mode exposes the shadowRoot property for debugging; 'closed' hides it entirely. Shadow DOM is the mechanism that makes encapsulated, drop-in UI components possible in plain browsers without a framework.
Common Misconception
Why It Matters
Common Mistakes
- Trying to style shadow-DOM content with page CSS — use CSS custom properties or the ::part() selector with a matching part="…" attribute on the inner element.
- Using document.querySelector() to find shadow-DOM elements — you must traverse through the host's shadowRoot property.
- Attaching shadow root twice on the same element — attachShadow() throws on a second call; check element.shadowRoot first.
- Forgetting the <slot> element for projected content — without a slot, children placed inside the host in markup are not rendered.
- Assuming focus and form participation work automatically — form-associated custom elements need the ElementInternals API; plain shadow trees do not submit values with forms.
Avoid When
- You control both the page and the component — regular CSS-in-JS or scoped stylesheets are simpler.
- SEO-critical content needs to be inside — shadow trees render fine but some older crawlers and tools have gaps; verify your target audience.
When To Use
- Building reusable UI widgets that must survive being dropped into unknown CSS environments.
- Creating design-system components where internal structure should be an implementation detail.
- Wrapping third-party content that you do not want page styles to leak into.
Code Examples
<!-- Global CSS breaks widget internals -->
<style>.label { color: red; }</style>
<my-widget><span class="label">hello</span></my-widget>
class MyWidget extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
.label { color: var(--label-color, black); }
</style>
<span class="label" part="label"><slot></slot></span>
`;
}
}
customElements.define('my-widget', MyWidget);
// Page CSS can still theme it:
// my-widget::part(label) { color: blue; }