Ты встраиваешь чат-виджет от стороннего сервиса на свой сайт. Без Shadow DOM его стили сломают твой CSS, а твои глобальные стили испортят виджет. С Shadow DOM — полная изоляция: стили снаружи не проникают внутрь, стили внутри не вытекают наружу. Именно так устроены <video>, <input type="range"> и другие нативные элементы браузера.
В крупных проектах CSS-конфликты — постоянная боль. Класс .button на одной странице ломает кнопку в другом компоненте. Shadow DOM создаёт изолированное дерево DOM, полностью независимое от глобальных стилей.
class MyWidget extends HTMLElement {
connectedCallback() {
// mode: 'open' — shadow root доступен через element.shadowRoot
// mode: 'closed' — shadow root недоступен снаружи (element.shadowRoot === null)
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
/* Эти стили изолированы — не влияют на p вне компонента */
p { color: red; font-size: 14px; }
</style>
<p>Содержимое виджета</p>
`
}
}Ключевое свойство Shadow DOM:
// Глобальный CSS страницы:
// p { color: blue; font-size: 20px; }
// Внутри Shadow DOM:
// p { color: red; }
// Параграф внутри компонента → красный (свои стили)
// Параграфы снаружи → синие (не затронуты)
// Аналогично: стили компонента не "вытекают":
// .card { background: white } — не применится к .card вне компонента:host позволяет стилизовать элемент-хост изнутри Shadow DOM:
shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
:host(.primary) {
border-color: #0066cc;
}
</style>
<slot></slot>
`CSS переменные (--variable) — стандартный способ передать стили снаружи внутрь:
// На странице:
// my-button { --btn-color: #0066cc; --btn-radius: 8px; }
// Внутри Shadow DOM:
shadow.innerHTML = `
<style>
button {
background: var(--btn-color, #333); /* снаружи или дефолт */
border-radius: var(--btn-radius, 4px);
}
</style>
<button><slot></slot></button>
`Ошибка 1: Ожидать, что глобальные стили работают внутри Shadow DOM
// Глобальный CSS: .primary { color: blue }
// Это НЕ применится к элементу внутри Shadow DOM
// Для стилизации изнутри используй :host(.primary) {}
// Для передачи снаружи используй CSS Custom PropertiesОшибка 2: mode: 'closed' для компонентов, которым нужен доступ
// closed блокирует element.shadowRoot — никто не достучится
// Даже сам компонент должен хранить ссылку на shadow root
class MyEl extends HTMLElement {
connectedCallback() {
this._shadow = this.attachShadow({ mode: 'closed' })
this._shadow.innerHTML = '<slot></slot>'
// element.shadowRoot === null — нужно использовать this._shadow
}
}Ошибка 3: querySelector не находит элементы в Shadow DOM
// НЕВЕРНО — document.querySelector не проникает в Shadow DOM
const btn = document.querySelector('my-widget button') // null!
// ВЕРНО — ищем внутри shadow root
const shadow = widget.shadowRoot
const btn2 = shadow.querySelector('button') // работает<ds-button>, <ds-modal> — изолированные компоненты без CSS-конфликтов<video>, <input range>, <details> — всё это Shadow DOM под капотомСимуляция Shadow DOM: изоляция стилей, :host, CSS-переменные, тематизация компонентов
// Симуляция Shadow DOM без реального DOM
// Демонстрируем концепцию инкапсуляции через объектную структуру
// ===== Фабрика элементов =====
function createElement(tagName) {
return {
tagName: tagName.toUpperCase(),
_attrs: {},
_cssVars: {},
shadowRoot: null,
attachShadow(options) {
const shadow = {
mode: options.mode || 'open',
host: this,
_html: '',
_styles: '',
get innerHTML() { return this._html },
set innerHTML(v) {
this._html = v
const m = v.match(/<style[^>]*>([sS]*?)</style>/i)
this._styles = m ? m[1].trim() : ''
},
}
this.shadowRoot = (options.mode === 'open') ? shadow : null
return shadow
},
setAttribute(k, v) { this._attrs[k] = String(v) },
getAttribute(k) { return this._attrs[k] ?? null },
setCSSVar(k, v) { this._cssVars[k] = v },
getCSSVar(k) { return this._cssVars[k] ?? null },
}
}
// ===== 1. Изоляция стилей =====
console.log('=== Изоляция стилей ===')
const PAGE_STYLES = { p: { color: 'blue', fontSize: '20px' } }
console.log('Глобальный стиль страницы: p { color:', PAGE_STYLES.p.color, '}')
const widget = createElement('my-widget')
const shadow = widget.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
p { color: red; font-size: 14px; }
.title { font-weight: bold; }
</style>
<p class="title">Заголовок виджета</p>
<p>Тело виджета</p>`
console.log('mode:', shadow.mode) // open
console.log('shadowRoot доступен:', widget.shadowRoot !== null) // true
console.log('Shadow DOM стили изолированы: p внутри → red (не blue от страницы)')
// ===== 2. mode: closed =====
console.log('\n=== mode: closed ===')
const secret = createElement('secret-widget')
const closedShadow = secret.attachShadow({ mode: 'closed' })
closedShadow.innerHTML = '<p>Скрытое содержимое</p>'
console.log('secret.shadowRoot:', secret.shadowRoot) // null
console.log('Снаружи shadowRoot недоступен — инкапсуляция работает')
// ===== 3. CSS Custom Properties =====
console.log('\n=== CSS Custom Properties ===')
const btn = createElement('my-button')
// Снаружи задаём CSS-переменные (в реальном CSS: my-button { --color: red })
btn.setCSSVar('--btn-color', '#0066cc')
btn.setCSSVar('--btn-radius', '8px')
btn.setCSSVar('--btn-padding', '10px 20px')
const btnShadow = btn.attachShadow({ mode: 'open' })
btnShadow.innerHTML = `
<style>
:host { display: inline-block; }
button {
background: var(--btn-color, #333);
border-radius: var(--btn-radius, 4px);
padding: var(--btn-padding, 8px 16px);
color: white; border: none; cursor: pointer;
}
</style>
<button><slot>Кнопка</slot></button>`
// CSS-переменные "пробивают" Shadow DOM
function resolveVar(shadow, varName) {
const hostVal = shadow.host.getCSSVar(varName)
if (hostVal) return hostVal + ' (с хоста)'
const fallbackMatch = shadow._styles.match(
new RegExp('var\\(' + varName.replace(/[-[]{}()*+?.,\^$|#s]/g, '\\$&') + ',\\s*([^)]+)\\)')
)
return fallbackMatch ? fallbackMatch[1].trim() + ' (fallback)' : 'не задано'
}
console.log('--btn-color:', btn.getCSSVar('--btn-color')) // #0066cc
console.log('--btn-radius:', btn.getCSSVar('--btn-radius')) // 8px
// ===== 4. Тематизация: один компонент, разные темы =====
console.log('\n=== Тематизация через CSS Custom Properties ===')
class ThemedCard {
constructor(theme = {}) {
this.host = createElement('themed-card')
this.shadow = this.host.attachShadow({ mode: 'open' })
Object.entries(theme).forEach(([k, v]) => this.host.setCSSVar(k, v))
this._render()
}
_render() {
this.shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px solid var(--card-border, #ddd);
border-radius: var(--card-radius, 8px);
overflow: hidden;
}
.header {
background: var(--card-header-bg, #f5f5f5);
color: var(--card-header-color, #333);
padding: 12px 16px;
font-weight: bold;
}
.body {
padding: 16px;
color: var(--card-body-color, #555);
}
</style>
<div class="header"><slot name="title">Без заголовка</slot></div>
<div class="body"><slot>Нет содержимого</slot></div>`
}
describe(name) {
const vars = Object.entries(this.host._cssVars)
console.log(` ${name}:`)
console.log(` mode: ${this.shadow.mode}`)
console.log(` CSS-переменные (снаружи): ${vars.map(([k,v]) => `${k}=${v}`).join(', ')}`)
console.log(` Изолирован от других карточек: ДА`)
}
}
const lightCard = new ThemedCard({
'--card-header-bg': '#e8f4fd',
'--card-header-color': '#0066cc',
'--card-border': '#b3d9f7',
})
const darkCard = new ThemedCard({
'--card-header-bg': '#1a1a2e',
'--card-header-color': '#e0e0ff',
'--card-border': '#444',
'--card-body-color': '#bbb',
})
lightCard.describe('Light Theme')
darkCard.describe('Dark Theme')
console.log('\nОба компонента изолированы — стили не конфликтуют')Ты встраиваешь чат-виджет от стороннего сервиса на свой сайт. Без Shadow DOM его стили сломают твой CSS, а твои глобальные стили испортят виджет. С Shadow DOM — полная изоляция: стили снаружи не проникают внутрь, стили внутри не вытекают наружу. Именно так устроены <video>, <input type="range"> и другие нативные элементы браузера.
В крупных проектах CSS-конфликты — постоянная боль. Класс .button на одной странице ломает кнопку в другом компоненте. Shadow DOM создаёт изолированное дерево DOM, полностью независимое от глобальных стилей.
class MyWidget extends HTMLElement {
connectedCallback() {
// mode: 'open' — shadow root доступен через element.shadowRoot
// mode: 'closed' — shadow root недоступен снаружи (element.shadowRoot === null)
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
/* Эти стили изолированы — не влияют на p вне компонента */
p { color: red; font-size: 14px; }
</style>
<p>Содержимое виджета</p>
`
}
}Ключевое свойство Shadow DOM:
// Глобальный CSS страницы:
// p { color: blue; font-size: 20px; }
// Внутри Shadow DOM:
// p { color: red; }
// Параграф внутри компонента → красный (свои стили)
// Параграфы снаружи → синие (не затронуты)
// Аналогично: стили компонента не "вытекают":
// .card { background: white } — не применится к .card вне компонента:host позволяет стилизовать элемент-хост изнутри Shadow DOM:
shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
:host(.primary) {
border-color: #0066cc;
}
</style>
<slot></slot>
`CSS переменные (--variable) — стандартный способ передать стили снаружи внутрь:
// На странице:
// my-button { --btn-color: #0066cc; --btn-radius: 8px; }
// Внутри Shadow DOM:
shadow.innerHTML = `
<style>
button {
background: var(--btn-color, #333); /* снаружи или дефолт */
border-radius: var(--btn-radius, 4px);
}
</style>
<button><slot></slot></button>
`Ошибка 1: Ожидать, что глобальные стили работают внутри Shadow DOM
// Глобальный CSS: .primary { color: blue }
// Это НЕ применится к элементу внутри Shadow DOM
// Для стилизации изнутри используй :host(.primary) {}
// Для передачи снаружи используй CSS Custom PropertiesОшибка 2: mode: 'closed' для компонентов, которым нужен доступ
// closed блокирует element.shadowRoot — никто не достучится
// Даже сам компонент должен хранить ссылку на shadow root
class MyEl extends HTMLElement {
connectedCallback() {
this._shadow = this.attachShadow({ mode: 'closed' })
this._shadow.innerHTML = '<slot></slot>'
// element.shadowRoot === null — нужно использовать this._shadow
}
}Ошибка 3: querySelector не находит элементы в Shadow DOM
// НЕВЕРНО — document.querySelector не проникает в Shadow DOM
const btn = document.querySelector('my-widget button') // null!
// ВЕРНО — ищем внутри shadow root
const shadow = widget.shadowRoot
const btn2 = shadow.querySelector('button') // работает<ds-button>, <ds-modal> — изолированные компоненты без CSS-конфликтов<video>, <input range>, <details> — всё это Shadow DOM под капотомСимуляция Shadow DOM: изоляция стилей, :host, CSS-переменные, тематизация компонентов
// Симуляция Shadow DOM без реального DOM
// Демонстрируем концепцию инкапсуляции через объектную структуру
// ===== Фабрика элементов =====
function createElement(tagName) {
return {
tagName: tagName.toUpperCase(),
_attrs: {},
_cssVars: {},
shadowRoot: null,
attachShadow(options) {
const shadow = {
mode: options.mode || 'open',
host: this,
_html: '',
_styles: '',
get innerHTML() { return this._html },
set innerHTML(v) {
this._html = v
const m = v.match(/<style[^>]*>([sS]*?)</style>/i)
this._styles = m ? m[1].trim() : ''
},
}
this.shadowRoot = (options.mode === 'open') ? shadow : null
return shadow
},
setAttribute(k, v) { this._attrs[k] = String(v) },
getAttribute(k) { return this._attrs[k] ?? null },
setCSSVar(k, v) { this._cssVars[k] = v },
getCSSVar(k) { return this._cssVars[k] ?? null },
}
}
// ===== 1. Изоляция стилей =====
console.log('=== Изоляция стилей ===')
const PAGE_STYLES = { p: { color: 'blue', fontSize: '20px' } }
console.log('Глобальный стиль страницы: p { color:', PAGE_STYLES.p.color, '}')
const widget = createElement('my-widget')
const shadow = widget.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
p { color: red; font-size: 14px; }
.title { font-weight: bold; }
</style>
<p class="title">Заголовок виджета</p>
<p>Тело виджета</p>`
console.log('mode:', shadow.mode) // open
console.log('shadowRoot доступен:', widget.shadowRoot !== null) // true
console.log('Shadow DOM стили изолированы: p внутри → red (не blue от страницы)')
// ===== 2. mode: closed =====
console.log('\n=== mode: closed ===')
const secret = createElement('secret-widget')
const closedShadow = secret.attachShadow({ mode: 'closed' })
closedShadow.innerHTML = '<p>Скрытое содержимое</p>'
console.log('secret.shadowRoot:', secret.shadowRoot) // null
console.log('Снаружи shadowRoot недоступен — инкапсуляция работает')
// ===== 3. CSS Custom Properties =====
console.log('\n=== CSS Custom Properties ===')
const btn = createElement('my-button')
// Снаружи задаём CSS-переменные (в реальном CSS: my-button { --color: red })
btn.setCSSVar('--btn-color', '#0066cc')
btn.setCSSVar('--btn-radius', '8px')
btn.setCSSVar('--btn-padding', '10px 20px')
const btnShadow = btn.attachShadow({ mode: 'open' })
btnShadow.innerHTML = `
<style>
:host { display: inline-block; }
button {
background: var(--btn-color, #333);
border-radius: var(--btn-radius, 4px);
padding: var(--btn-padding, 8px 16px);
color: white; border: none; cursor: pointer;
}
</style>
<button><slot>Кнопка</slot></button>`
// CSS-переменные "пробивают" Shadow DOM
function resolveVar(shadow, varName) {
const hostVal = shadow.host.getCSSVar(varName)
if (hostVal) return hostVal + ' (с хоста)'
const fallbackMatch = shadow._styles.match(
new RegExp('var\\(' + varName.replace(/[-[]{}()*+?.,\^$|#s]/g, '\\$&') + ',\\s*([^)]+)\\)')
)
return fallbackMatch ? fallbackMatch[1].trim() + ' (fallback)' : 'не задано'
}
console.log('--btn-color:', btn.getCSSVar('--btn-color')) // #0066cc
console.log('--btn-radius:', btn.getCSSVar('--btn-radius')) // 8px
// ===== 4. Тематизация: один компонент, разные темы =====
console.log('\n=== Тематизация через CSS Custom Properties ===')
class ThemedCard {
constructor(theme = {}) {
this.host = createElement('themed-card')
this.shadow = this.host.attachShadow({ mode: 'open' })
Object.entries(theme).forEach(([k, v]) => this.host.setCSSVar(k, v))
this._render()
}
_render() {
this.shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px solid var(--card-border, #ddd);
border-radius: var(--card-radius, 8px);
overflow: hidden;
}
.header {
background: var(--card-header-bg, #f5f5f5);
color: var(--card-header-color, #333);
padding: 12px 16px;
font-weight: bold;
}
.body {
padding: 16px;
color: var(--card-body-color, #555);
}
</style>
<div class="header"><slot name="title">Без заголовка</slot></div>
<div class="body"><slot>Нет содержимого</slot></div>`
}
describe(name) {
const vars = Object.entries(this.host._cssVars)
console.log(` ${name}:`)
console.log(` mode: ${this.shadow.mode}`)
console.log(` CSS-переменные (снаружи): ${vars.map(([k,v]) => `${k}=${v}`).join(', ')}`)
console.log(` Изолирован от других карточек: ДА`)
}
}
const lightCard = new ThemedCard({
'--card-header-bg': '#e8f4fd',
'--card-header-color': '#0066cc',
'--card-border': '#b3d9f7',
})
const darkCard = new ThemedCard({
'--card-header-bg': '#1a1a2e',
'--card-header-color': '#e0e0ff',
'--card-border': '#444',
'--card-body-color': '#bbb',
})
lightCard.describe('Light Theme')
darkCard.describe('Dark Theme')
console.log('\nОба компонента изолированы — стили не конфликтуют')Используя функцию `createElement` из примера, реализуй компонент `BadgeElement`. Требования: - Атрибуты: `text` (текст), `color` (цвет, дефолт `"gray"`), `size` (`"small"`/`"medium"`/`"large"`, дефолт `"medium"`) - CSS-переменные хоста `--badge-color` и `--badge-size` имеют приоритет над атрибутами - Метод `render()` — логирует итоговые стили и возвращает объект стилей - Метод `describe()` — логирует все параметры компонента
attachShadow({ mode: "open" }). setAttribute(k, v) и setCSSVar(k, v) в конструкторе. describe(): getAttribute("text"), getAttribute("color"), getAttribute("size"). render() — getCSSVar имеет приоритет над getAttribute