Клавиша / esc

Shadow DOM

Как Shadow DOM помогает создавать изолированные компоненты и защищает их от внешних вмешательств.

Время чтения: 9 мин

Что такое Shadow DOM?

Скопировано

Представьте, что вы создали кастомный компонент — например, кнопку с анимацией или сложный виджет. Вы хотите, чтобы этот компонент работал на любой странице, независимо от того, какие стили или скрипты уже есть на сайте. Но как защитить внутреннюю структуру компонента от случайного вмешательства?

Именно для этого и был создан Shadow DOM — технология, которая позволяет создавать изолированные деревья DOM, скрытые от основного документа. Это как коробка с секретом: снаружи вы видите только компонент, а внутри у него своя жизнь, свои стили и своя структура.

Shadow DOM решает три основные проблемы:

  • Инкапсуляция стилей — CSS внутри компонента не влияет на внешние элементы
  • Скрытие структуры — внутренние элементы недоступны из основного DOM
  • Переиспользование — компонент работает одинаково в любом контексте

Как работает Shadow DOM?

Скопировано

Основные концепции

Скопировано

Shadow DOM состоит из нескольких ключевых частей:

  • Shadow Host — обычный DOM-элемент, к которому прикрепляется Shadow DOM;
  • Shadow Tree — дерево DOM внутри Shadow DOM;
  • Shadow Boundary — граница между Shadow DOM и обычным DOM;
  • Shadow Root — корневой узел Shadow Tree.
        
          
          // Создание Shadow DOMconst host = document.querySelector('#my-element');const shadow = host.attachShadow({ mode: 'open' });// Теперь у нас есть изолированное дерево DOMshadow.innerHTML = `  <style>    div { color: red; }  </style>  <div>Это содержимое Shadow DOM</div>`;
          // Создание Shadow DOM
const host = document.querySelector('#my-element');
const shadow = host.attachShadow({ mode: 'open' });

// Теперь у нас есть изолированное дерево DOM
shadow.innerHTML = `
  <style>
    div { color: red; }
  </style>
  <div>Это содержимое Shadow DOM</div>
`;

        
        
          
        
      

Режимы доступа

Скопировано

Shadow DOM может работать в двух режимах:

Открытый режим (mode: 'open'):

        
          
          const shadow = element.attachShadow({ mode: 'open' });console.log(element.shadowRoot); // ShadowRoot
          const shadow = element.attachShadow({ mode: 'open' });
console.log(element.shadowRoot); // ShadowRoot

        
        
          
        
      

Позволяет получить доступ к ShadowRoot извне через свойство element.shadowRoot. Это удобно для отладки и взаимодействия с внутренним содержимым компонента из внешнего кода.

Закрытый режим (mode: 'closed'):

        
          
          const shadow = element.attachShadow({ mode: 'closed' });console.log(element.shadowRoot); // null
          const shadow = element.attachShadow({ mode: 'closed' });
console.log(element.shadowRoot); // null

        
        
          
        
      

В закрытом режиме получить доступ к ShadowRoot извне невозможно — свойство element.shadowRoot всегда возвращает null. Это обеспечивает максимальную изоляцию внутренней структуры компонента: никакой внешний скрипт не сможет напрямую обратиться к содержимому Shadow DOM.

Открытый режим удобен для отладки, закрытый — для максимальной инкапсуляции.

Инкапсуляция от JavaScript

Скопировано

Одна из главных особенностей Shadow DOM — элементы внутри него недоступны для обычных DOM-методов.

        
          
          // Создаём Shadow DOMconst host = document.querySelector('#host');const shadow = host.attachShadow({ mode: 'open' });shadow.innerHTML = '<span>Я в Shadow DOM</span>';// Этот код НЕ найдёт элемент в Shadow DOMconst spans = document.querySelectorAll('span');console.log(spans.length); // 0// Но этот код найдётconst shadowSpans = host.shadowRoot.querySelectorAll('span');console.log(shadowSpans.length); // 1
          // Создаём Shadow DOM
const host = document.querySelector('#host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<span>Я в Shadow DOM</span>';

// Этот код НЕ найдёт элемент в Shadow DOM
const spans = document.querySelectorAll('span');
console.log(spans.length); // 0

// Но этот код найдёт
const shadowSpans = host.shadowRoot.querySelectorAll('span');
console.log(shadowSpans.length); // 1

        
        
          
        
      

Это означает, что внешние скрипты не могут случайно сломать ваш компонент, изменив его внутренние элементы.

Инкапсуляция от CSS

Скопировано

Аналогично работает и с CSS — стили из основного документа не проникают в Shadow DOM.

        
          
          <style>  /* Эти стили НЕ повлияют на элементы в Shadow DOM */  span {    color: blue;    font-weight: bold;  }</style><div id="host"></div>
          <style>
  /* Эти стили НЕ повлияют на элементы в Shadow DOM */
  span {
    color: blue;
    font-weight: bold;
  }
</style>

<div id="host"></div>

        
        
          
        
      
        
          
          const host = document.querySelector('#host');const shadow = host.attachShadow({ mode: 'open' });shadow.innerHTML = `  <style>    /* Эти стили работают только внутри Shadow DOM */    span {      color: red;      border: 1px solid black;    }  </style>  <span>Красный текст с рамкой</span>`;
          const host = document.querySelector('#host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
  <style>
    /* Эти стили работают только внутри Shadow DOM */
    span {
      color: red;
      border: 1px solid black;
    }
  </style>
  <span>Красный текст с рамкой</span>
`;

        
        
          
        
      

В результате элемент в Shadow DOM будет красным с рамкой, а не синим и жирным, как в основном документе.

Исключения из инкапсуляции

Скопировано

Несмотря на изоляцию, некоторые CSS-свойства всё же проникают в Shadow DOM:

  • CSS Custom Properties (переменные) наследуются по умолчанию:

    Скопировано
        
          
          /* В основном документе */:root {  --primary-color: blue;}
          /* В основном документе */
:root {
  --primary-color: blue;
}

        
        
          
        
      
        
          
          // В Shadow DOM переменная будет доступнаshadow.innerHTML = `  <style>    span {      color: var(--primary-color); /* Работает! */    }  </style>  <span>Текст</span>`;
          // В Shadow DOM переменная будет доступна
shadow.innerHTML = `
  <style>
    span {
      color: var(--primary-color); /* Работает! */
    }
  </style>
  <span>Текст</span>
`;

        
        
          
        
      
  • Наследуемые CSS-свойства также проникают через shadow boundary:

    Скопировано
        
          
          /* Эти стили повлияют на Shadow DOM */body {  color: green;  font-family: Arial;  text-align: center;}
          /* Эти стили повлияют на Shadow DOM */
body {
  color: green;
  font-family: Arial;
  text-align: center;
}

        
        
          
        
      

К наследуемым свойствам относятся:

  • color
  • font, font-family, font-size и другие font-*
  • text-align, text-indent, text-transform
  • line-height, letter-spacing, word-spacing
  • cursor, direction, visibility

Если нужно полностью изолировать компонент от наследуемых стилей, используйте all: initial:

        
          
          :host {  all: initial; /* Сбрасывает все наследуемые стили */}
          :host {
  all: initial; /* Сбрасывает все наследуемые стили */
}

        
        
          
        
      

Доступность

Скопировано

Shadow DOM изолирует не только структуру и стили, но и область видимости идентификаторов (id). Это значит, что такие атрибуты, как aria-labelledby, aria-describedby, for и другие, которые ссылаются на элементы по id, работают только внутри одного и того же дерева (shadow tree или light DOM).

Например, если у вас есть:

        
          
          <!-- В light DOM --><div id="label">Заголовок</div><my-element aria-labelledby="label"></my-element>
          <!-- В light DOM -->
<div id="label">Заголовок</div>
<my-element aria-labelledby="label"></my-element>

        
        
          
        
      

и внутри кастомного элемента:

        
          
          // Внутри shadow DOM<span id="label">Внутренний заголовок</span><input aria-labelledby="label">
          // Внутри shadow DOM
<span id="label">Внутренний заголовок</span>
<input aria-labelledby="label">

        
        
          
        
      
  • Если aria-labelledby="label" ссылается на элемент вне текущего shadow tree, связь не сработает.
  • Аналогично, если элемент с нужным id находится в другом shadow tree, связь также не установится.

Стилизация Shadow DOM

Скопировано

Внутренние стили

Скопировано

Самый простой способ — добавить <style> прямо в Shadow DOM:

        
          
          const shadow = element.attachShadow({ mode: 'open' });shadow.innerHTML = `  <style>    :host {      display: block;      border: 1px solid #ccc;    }    .internal {      color: red;    }  </style>  <div class="internal">Содержимое</div>`;
          const shadow = element.attachShadow({ mode: 'open' });
shadow.innerHTML = `
  <style>
    :host {
      display: block;
      border: 1px solid #ccc;
    }
    .internal {
      color: red;
    }
  </style>
  <div class="internal">Содержимое</div>
`;

        
        
          
        
      

Конструктивные таблицы стилей

Скопировано

Для более сложных случаев можно использовать adoptedStyleSheets:

        
          
          const sheet = new CSSStyleSheet();sheet.replaceSync(`  :host {    display: block;    padding: 16px;  }  .component {    background: #f0f0f0;  }`);const shadow = element.attachShadow({ mode: 'open' });shadow.adoptedStyleSheets = [sheet];
          const sheet = new CSSStyleSheet();
sheet.replaceSync(`
  :host {
    display: block;
    padding: 16px;
  }
  .component {
    background: #f0f0f0;
  }
`);

const shadow = element.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sheet];

        
        
          
        
      

Псевдоклассы :host и :host()

Скопировано

Позволяет стилизовать сам элемент-хост изнутри Shadow DOM:

        
          
          :host {  display: block;  border: 2px solid blue;}:host(:hover) {  border-color: red;}
          :host {
  display: block;
  border: 2px solid blue;
}

:host(:hover) {
  border-color: red;
}

        
        
          
        
      

Shadow DOM и кастомные элементы

Скопировано

Shadow DOM особенно полезен в сочетании с customElements. Вот пример компонента с изолированной структурой:

        
          
          class MyCard extends HTMLElement {  constructor() {    super();    const shadow = this.attachShadow({ mode: 'open' });    shadow.innerHTML = `      <style>        :host {          display: block;          border: 1px solid #ccc;          border-radius: 8px;          padding: 16px;          max-width: 300px;        }        .title {          font-weight: bold;          margin-bottom: 8px;        }        .content {          color: #666;        }      </style>      <div class="title">        <slot name="title">Заголовок</slot>      </div>      <div class="content">        <slot>Содержимое карточки</slot>      </div>    `;  }}customElements.define('my-card', MyCard);
          class MyCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #ccc;
          border-radius: 8px;
          padding: 16px;
          max-width: 300px;
        }
        .title {
          font-weight: bold;
          margin-bottom: 8px;
        }
        .content {
          color: #666;
        }
      </style>
      <div class="title">
        <slot name="title">Заголовок</slot>
      </div>
      <div class="content">
        <slot>Содержимое карточки</slot>
      </div>
    `;
  }
}

customElements.define('my-card', MyCard);

        
        
          
        
      
        
          
          <my-card>  <span slot="title">Моя карточка</span>  <p>Это содержимое карточки</p></my-card>
          <my-card>
  <span slot="title">Моя карточка</span>
  <p>Это содержимое карточки</p>
</my-card>

        
        
          
        
      

Слоты для гибкости

Скопировано

Слоты (<slot>) позволяют вставлять внешний контент в Shadow DOM:

        
          
          shadow.innerHTML = `  <div class="header">    <slot name="header">Заголовок по умолчанию</slot>  </div>  <div class="body">    <slot>Содержимое по умолчанию</slot>  </div>`;
          shadow.innerHTML = `
  <div class="header">
    <slot name="header">Заголовок по умолчанию</slot>
  </div>
  <div class="body">
    <slot>Содержимое по умолчанию</slot>
  </div>
`;

        
        
          
        
      
        
          
          <my-component>  <h1 slot="header">Мой заголовок</h1>  <p>Моё содержимое</p></my-component>
          <my-component>
  <h1 slot="header">Мой заголовок</h1>
  <p>Моё содержимое</p>
</my-component>

        
        
          
        
      

Декларативный Shadow DOM

Скопировано

В современных браузерах можно создавать Shadow DOM прямо в HTML с помощью <template>:

        
          
          <div id="host">  <template shadowrootmode="open">    <style>      span { color: red; }    </style>    <span>Я в декларативном Shadow DOM</span>  </template></div>
          <div id="host">
  <template shadowrootmode="open">
    <style>
      span { color: red; }
    </style>
    <span>Я в декларативном Shadow DOM</span>
  </template>
</div>

        
        
          
        
      

Это особенно полезно для серверного рендеринга.

Когда использовать Shadow DOM?

Скопировано

Используйте Shadow DOM когда:

  • Создаёте переиспользуемые компоненты
  • Нужна полная изоляция стилей
  • Компонент должен работать в любом контексте
  • Хотите защитить внутреннюю структуру от внешних вмешательств

НЕ используйте когда:

  • Нужна максимальная гибкость стилизации
  • Компонент должен адаптироваться к дизайн-системе сайта
  • Требуется простое решение без сложностей
Поддержка в браузерах:
  • Chrome 53, поддерживается
  • Edge 79, поддерживается
  • Firefox 63, поддерживается
  • Safari 10, поддерживается
О Baseline