Suggest an editImprove this articleRefine the answer for “What is shadow DOM in web development”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Shadow DOM** is a browser API that attaches a separate, isolated DOM subtree to an element, keeping its CSS and HTML scoped away from the rest of the page. ```js const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>p { color: red; }</style> <p>This paragraph ignores all page styles.</p> `; ``` **Key:** CSS cannot cross the shadow boundary in either direction. That is the foundation of Web Components style isolation.Shown above the full answer for quick recall.Answer (EN)Image**Shadow DOM** is a browser API that attaches a separate, encapsulated DOM subtree to a host element, keeping its HTML, CSS, and JS isolated from the rest of the page. ## Theory ### TL;DR - Think of it like a soundproof apartment in a busy building: internal styles stay inside, external styles cannot get in. - Regular DOM mixes all CSS rules globally; Shadow DOM draws a hard boundary at the element level. - The browser allocates a separate node tree per shadow root; CSS selectors stop at that boundary and do not cross it. - `mode: 'open'` exposes the shadow root to external JS; `mode: 'closed'` hides it, returning `null` for `host.shadowRoot`. - Use it for reusable UI components, third-party embeds, or micro-frontends where style collisions are a real problem. ### Quick example ```html <!DOCTYPE html> <html> <head> <style>p { color: blue !important; }</style> </head> <body> <p>Regular paragraph (blue, affected by page CSS).</p> <div id="host"></div> <script> const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>p { color: red; }</style> <p>Shadow paragraph (red, page CSS has zero effect here).</p> `; </script> </body> </html> ``` The outer `!important` rule cannot reach inside the shadow root. The red paragraph ignores it completely. That is the whole point. ### Key difference Without Shadow DOM, all CSS competes globally through inheritance and specificity. One badly named `.btn` class in a third-party widget can override your button styles. Shadow DOM draws a hard boundary: the browser renders a distinct tree where internal styles match only internal elements, and external styles never pierce in. The only bridge is `host.shadowRoot`, and only when `mode` is `'open'`. ### When to use - Reusable UI widgets (dropdowns, modals, date pickers) that ship with their own styles and must not clash with host page CSS. - Third-party embeds (chat widgets, payment buttons) that load on unknown host pages with unpredictable stylesheets. - Micro-frontends where multiple teams share a page. Without Shadow DOM, two teams accidentally defining `.card` with different margins will conflict silently. I have seen this cause bugs that take hours to trace back to the source. - Skip it for simple static pages or tightly controlled apps where isolation is not needed. Plain DOM has no overhead. ### Open vs closed mode `{ mode: 'open' }` exposes `host.shadowRoot` for external JS access. Most custom elements and frameworks use this. `{ mode: 'closed' }` makes `host.shadowRoot` return `null`, blocking external queries. Chrome DevTools panels use this pattern to prevent page JS from interfering with the DevTools UI. Closed mode is not a security boundary. Browser devtools can still inspect the tree. It only prevents accidental external access from scripts. ### Slots and light DOM projection A `<slot>` inside a shadow root acts as a placeholder for the host element's children. Those children (light DOM) get projected there at render time. ```html <div id="host"> <span>I am light DOM</span> </div> <script> const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>span { color: green; font-weight: bold; }</style> <slot></slot> `; </script> ``` The `<span>` renders green and bold because slotted light DOM inherits shadow styles. Use `::slotted(span)` to control that behavior explicitly instead of relying on inheritance. ### How the browser handles it Browsers allocate a separate `ShadowRoot` node linked to the host element. During rendering, the engine composes light DOM children into shadow `<slot>` elements but skips cross-boundary style resolution. `document.querySelector('p')` will not find paragraphs inside a shadow root. External JS access requires `mode: 'open'` and goes through `host.shadowRoot.querySelector('p')`. ### Common mistakes 1. **Querying shadow contents when mode is `'closed'`** ```js const el = document.createElement('div'); document.body.appendChild(el); el.attachShadow({ mode: 'closed' }); // el.shadowRoot === null el.shadowRoot.querySelector('p'); // TypeError: Cannot read properties of null ``` Fix: use `{ mode: 'open' }` when external access is needed, or pass DOM references through the component's own API. 2. **Expecting external CSS to reach inside the shadow root** ```css /* This selector cannot pierce the shadow boundary */ my-element * { color: black !important; } ``` To theme shadow components from outside, use CSS custom properties (they cross the boundary by design) or `::part()` on exported parts. 3. **Forgetting that slot projection applies shadow styles** ```html <my-el><p>Light child</p></my-el> ``` If `<my-el>` has a `<slot>`, that `<p>` renders inside the shadow and picks up shadow CSS. Use `::slotted(p) { color: inherit; }` to override that intentionally. 4. **Assuming nested shadow roots are fully isolated from each other** Inner shadow styles can interact with outer shadow via slot composition. Test rendering explicitly. Nesting shadows does not automatically double the isolation. ### Real-world usage - **Lit (Google)**: every component uses Shadow DOM by default, giving zero CSS conflicts across the component tree. - **Angular Elements**: wraps Angular components in Shadow DOM for embedding in non-Angular host pages. - **Chrome DevTools**: every panel runs in a closed shadow root so page JS and CSS cannot break the DevTools UI. - **Vaadin**: enterprise grid and form components ship consistent styles through Shadow DOM across any host application. ### Follow-up questions **Q:** What is the difference between `open` and `closed` mode? **A:** `open` exposes `host.shadowRoot` for external JS. `closed` returns `null` there. Neither mode prevents browser devtools from inspecting the tree. **Q:** How do CSS custom properties work across the shadow boundary? **A:** They cross the boundary by design. The host page sets `--primary-color: red` and shadow styles read it. That is the standard way to theme shadow DOM components without breaking isolation. **Q:** How do slots work and what is light DOM projection? **A:** A `<slot>` is a placeholder for the host's children. Unnamed slots take all direct children. Named slots use `slot="name"` on the child element and `<slot name="name">` in the shadow template. **Q:** Does Shadow DOM affect accessibility? **A:** ARIA on the host propagates correctly. Elements inside the shadow tree need explicit roles and labels. Screen readers traverse the flattened composed tree, not the shadow tree in isolation. **Q:** (Senior) How would you expose shadow internals for external theming without opening the full shadow root? **A:** Use `::part()`. Mark internal elements with `part="button"` in the shadow template. Host pages style them with `my-el::part(button) { background: var(--theme-color); }`. This gives explicit, controlled exposure without leaking the full internal structure. ## Examples ### Style isolation in 10 lines ```html <!DOCTYPE html> <html> <head> <style>p { color: blue; font-size: 24px; }</style> </head> <body> <p>Regular paragraph (blue, 24px).</p> <div id="host"></div> <script> const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>p { color: red; font-size: 14px; }</style> <p>Shadow paragraph (red, 14px). Page CSS cannot touch this.</p> `; </script> </body> </html> ``` Two paragraphs, two completely independent style scopes. No `!important` tricks. No class name gymnastics. ### Todo item as a custom element ```js class TodoItem extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> .todo { padding: 1rem; border: 1px solid #ccc; border-radius: 4px; } .done { text-decoration: line-through; opacity: 0.5; } </style> <div class="todo"> <input type="checkbox"> <span class="text"></span> </div> `; const text = shadow.querySelector('.text'); const checkbox = shadow.querySelector('input'); checkbox.addEventListener('change', () => { text.classList.toggle('done', checkbox.checked); }); } connectedCallback() { this.shadowRoot.querySelector('.text').textContent = this.getAttribute('todo') || 'Empty todo'; } } customElements.define('todo-item', TodoItem); ``` ```html <todo-item todo="Buy milk"></todo-item> <todo-item todo="Write tests"></todo-item> ``` Each `<todo-item>` runs in its own shadow root. Host page CSS cannot override the strikethrough style. No shared class names, no conflicts between components. ### Theming via CSS custom properties and ::part() ```js class FancyButton extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).innerHTML = ` <style> button { background: var(--btn-bg, #0070f3); /* falls back to blue */ color: var(--btn-color, white); padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; } </style> <button part="btn"><slot></slot></button> `; } } customElements.define('fancy-button', FancyButton); ``` Host page theming: ```css /* CSS custom properties cross the shadow boundary */ fancy-button { --btn-bg: #e53e3e; } /* ::part() targets exported parts explicitly */ fancy-button::part(btn) { border-radius: 0; } ``` This is how production component libraries handle theming without exposing full shadow internals. The component author decides what is public (`part="btn"`); everything else stays private.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.