Shadow DOM #
Shadow DOM — отдельное поддерево узлов, прикреплённое к хост-элементу через attachShadow. Стили и разметка внутри shadow root не смешиваются с остальной страницей: глобальный CSS не протекает внутрь, а внутренние селекторы не «выстреливают» наружу. Это основа инкапсуляции в Custom Elements.
attachShadow #
class DashboardTile extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
:host {
display: block;
border-radius: 8px;
padding: 12px;
background: var(--tile-bg, #1a1a2e);
}
h2 { margin: 0 0 8px; font-size: 1rem; }
.metric { font-size: 1.5rem; font-weight: 600; }
</style>
<h2><slot name="title">Виджет</slot></h2>
<p class="metric"><slot name="value">—</slot></p>
`
}
}
customElements.define('dashboard-tile', DashboardTile)
<dashboard-tile>
<span slot="title">Активные заказы</span>
<span slot="value">128</span>
</dashboard-tile>
mode: 'open' даёт доступ к shadow root через element.shadowRoot. В closed снаружи root не виден — только для жёсткой инкапсуляции; отладка сложнее.
Граница light DOM и shadow DOM #
| Light DOM | Shadow DOM | |
|---|---|---|
| Где задаётся | Дочерние узлы в HTML страницы | Внутри attachShadow |
| Стили | Глобальные таблицы CSS | Локальные <style> в shadow |
| Доступ из JS | element.children | element.shadowRoot.querySelector |
Узлы в light DOM не отображаются напрямую, если в shadow есть <slot> — браузер проецирует их в место слота. Подробнее — в template и slots.
Псевдокласс :host #
:host стилизует сам хост-элемент изнутри shadow:
:host {
display: inline-flex;
gap: 8px;
}
:host([variant='compact']) {
padding: 4px 8px;
font-size: 0.875rem;
}
:host-context(.dark-theme) реагирует на предка снаружи shadow (поддержка в Chromium; в других движках проверяйте актуальность).
CSS custom properties (пробивание стилей) #
Жёсткая изоляция не запрещает темизацию через переменные — они проходят через границу:
shadow.innerHTML = `
<style>
button {
background: var(--accent, #6366f1);
color: var(--on-accent, #fff);
}
</style>
<button><slot></slot></button>
`
<playlist-action style="--accent: #22c55e">Добавить в плейлист</playlist-action>
Так родитель задаёт палитру, не ломая внутреннюю вёрстку компонента.
События и compose #
События из shadow DOM по умолчанию не всплывают за пределы shadow (composed: false). Чтобы родитель поймал клик по внутренней кнопке, переотправьте событие или используйте composed: true:
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(
new CustomEvent('tile-action', { bubbles: true, composed: true })
)
})
Слушайте на хосте: tile.addEventListener('tile-action', handler).
Shadow DOM ≠ «Shadow DOM v0» #
Нативные элементы вроде <video> и <input type="date"> уже используют внутренний shadow tree — это user-agent shadow. Ваш код создаёт author shadow через attachShadow. Вызов attachShadow дважды на одном элементе невозможен: повторный connectedCallback должен проверять наличие root.
connectedCallback() {
if (this.shadowRoot) return
this.attachShadow({ mode: 'open' })
// ...
}
Итог #
attachShadow({ mode: 'open' | 'closed' })создаёт изолированное поддерево.- Стили в shadow не конфликтуют с глобальными;
:hostоформляет сам элемент. - Темизация снаружи — через CSS-переменные на хосте.
- Light DOM-дети попадают в shadow через
<slot>. - События наружу — с
composed: trueили кастомные события на хосте.