В современных сайтах часто используется много разных элементов, стилей и компонентов, и иногда бывает так, что стили одного элемента начинают влиять на другие. В итоге всё это накладывается друг на друга и страница начинает выглядеть странно: вёрстка плывёт, контент не на своём месте и всё такое.
Чтобы такого не случалось, разработчики изолируют стили элементов, используя теневой DOM (Shadow DOM). Это то же самое, что и обычный DOM, но защищённый от внешнего воздействия. Стили и скрипты внутри теневого DOM не влияют на остальные элементы на странице и не зависят от них. Такие элементы не сломают стили страницы, если их использовать в другом проекте. Рассказываем, что такое теневой DOM и как с ним работать на практике.
Что такое Document Object Model (DOM)
Для начала вспомним, что такое DOM.
DOM (Document Object Model) — это представление HTML-документа в виде иерархического дерева узлов, где каждый элемент на странице — это отдельный узел, или DOM-элемент.
Проще говоря, страница — это дерево, а каждый элемент на ней — это ветки и листья. Используя DOM, мы можем с помощью JS-скриптов дотянуться до любого элемента на странице и что-то с ним сделать: поменять текст, скрыть что-то или добавить новые части. Если нужно поменять заголовок на странице, это можно сделать через DOM, не перезагружая страницу. То есть DOM — это то, с помощью чего JavaScript может «общаться» с HTML-страницей и делать её интерактивной.
Схематично это можно нарисовать как-то так:
DOM появляется, когда браузер загружает HTML-документ. Браузер читает HTML-код и на его основе собирает для себя структуру в виде дерева. Затем всё это браузер помещает в объект document
, к которому мы можем обращаться, в котором можем искать нужные элементы и что-то с ними делать. Например, так:
// Находим первый элемент <h2> на странице и сохраняем его в переменную heading
let heading = document.querySelector('h2');
// Меняем текст внутри найденного элемента <h2> на "Новый заголовок"
heading.textContent = "Новый заголовок";
👉 А теперь самое интересное: DOM-элемент на странице может иметь не только свой видимый контент, но и скрытое, изолированное поддерево внутри. Это и есть теневой DOM, который в отличие от обычного, светлого, всегда скрыт.
Получается, что DOM-элемент на странице может иметь два дерева:
🌝 Light DOM (светлое дерево) — это обычное дерево элементов, которые мы видим в HTML-разметке и с которыми работаем через document.querySelector
или getElementsBy*
. Это стандартная часть DOM, которую можно посмотреть в инструментах разработчика.
🌚 Shadow DOM (теневой DOM) — это скрытое дерево элементов, которое создаётся разработчиком с помощью JavaScript. Оно изолировано от остального документа,поэтому его стили и скрипты не пересекаются с внешним кодом. Элементы в теневом DOM нельзя найти через обычные методы, поскольку они спрятаны внутри элемента, к которому прикреплены.
Дальше более подробно посмотрим, как это работает.
Как устроен Shadow DOM
Допустим, есть элемент <div>
. Этот <div>
— часть обычного DOM (светлого дерева). Если разработчик создаёт для него теневой DOM, то у этого <div>
появляется теневой корень, под которым строится теневое дерево. Это теневое дерево полностью скрыто внутри этого <div>
, хотя вся эта конструкция и остаётся в обычном DOM и не видна пользователю.
Элемент, к которому мы прикрепляем теневое дерево, называется shadow host
. Внутри него находится shadow root
— теневой корень, который прячет всю разметку и стили. Именно через этот узел происходит доступ к стилям, которые находятся внутри теневого DOM.
Разметка, стили и логика теневых элементов не влияют на остальную часть документа и сами не подвержены стороннему влиянию. Получается, что теневой DOM позволяет создавать отдельные мини-страницы внутри основной страницы, где элементы живут по своим правилам.
Shadow root
выполняет роль контейнера для всех элементов и стилей, которые спрятаны в теневом дереве. Он целиком отделён от основного DOM, поэтому стили и скрипты внутри него не могут даже случайно взаимодействовать с внешней частью страницы. Такой элемент компонент всегда будет выглядеть и работать одинаково, независимо от остальной части сайта.
Теневой DOM можно добавлять к разным элементам, поэтому на странице может быть сразу несколько теневых DOM:
Корневой элемент (Документ)
├── Обычный DOM (Light DOM)
│ ├── Другие элементы
│ ├── (Веб-компонент 1)
│ │ └── #shadow-root (Теневой DOM для компонента 1)
│ │ ├── (Изолированные стили для компонента 1)
│ │ ├── (Изолированный контент для компонента 1)
│ │ └── … (Другие изолированные элементы для компонента 1)
│ ├── (Веб-компонент 2)
│ │ └── #shadow-root (Теневой DOM для компонента 2)
│ │ ├── (Изолированные стили для компонента 2)
│ │ ├── (Изолированный контент для компонента 2)
│ │ └── … (Другие изолированные элементы для компонента 2)
│ └── …
└── …
Теперь посмотрим, зачем это нужно в реальной жизни.
Как используется Shadow DOM
Механизм теневого DOM используют при создании пользовательских компонентов — виджетов или элементов интерфейса, логика и стили которых должны быть изолированы от остального контента страницы.
Допустим, мы разрабатываем виджет поддержки клиентов, который будет размещаться на разных сайтах. Чтобы он везде работал корректно, мы изолируем все его внутренние элементы от остальной страницы. В теневой DOM положим разметку виджета (контейнер чата, поле ввода и кнопку отправки), стили этих элементов, и скрипты, управляющие поведением. Такая изоляция будет гарантировать, например, что кнопка отправки всегда будет выглядеть так, как задумано, независимо от того, какие правила для кнопок заданы на сайте. Логика отправки сообщений тоже защищена — внешний код не может вмешаться в работу виджета.
В большинстве случаев стили компонентов с теневым DOM можно посмотреть через инструменты разработчика. В Chrome в настройках нужно поставить галочку «Показывать теневой DOM», и если на странице будут элементы теневого DOM, то их можно будет посмотреть.
Теперь мы увидим всё, что находится на странице на самом деле:
Но есть один важный нюанс.
Теневой DOM можно создавать в двух режимах: open
и closed
. Когда режим открытый, то содержимое элемента можно не только увидеть в инструментах разработчика, но и программно получить к нему доступ через JavaScript. А вот при режиме closed
можно только посмотреть содержимое теневого DOM, но доступ к нему через код невозможен. Это значит, что никакие внешние скрипты, включая браузерные расширения, не смогут взаимодействовать с закрытым теневым DOM через код.
Часто такой трюк используется в вёрстке, чтобы скрыть стили рекламных элементов от блокировщиков рекламы. Поскольку блокировщики часто работают на основе анализа HTML-разметки и внешних стилей страницы, то не сразу могут обнаружить и удалить элементы, если те находятся в закрытом теневом DOM. Это создаёт дополнительную защиту от автоматических фильтров.
Теперь проверим всё это на практике: создадим свой элемент с теневым DOM и посмотрим, как в зависимости от режима будет меняться его отображение.
Как создать Shadow DOM
Теневой DOM можно прикрепить только к тем элементам, которые могут быть контейнерами для этой изолированной области. Это обычные HTML-элементы <div>, <span>, <section>, <article>, <header>, <footer>, <aside>, <nav>, <main>
или пользовательские элементы (веб-компоненты).
Сделаем простую HTML-страницу и добавим в неё блок <div>
:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Теневой DOM</title>
</head>
<body>
<!-- Контейнер для теневого DOM -->
<div id="shadow-element"></div>
<script>
// Здесь настроим теневой DOM, добавим в него стили и элементы
// и пропишем логику для обработки событий внутри теневого DOM
</script>
</body>
</html>
Страница пока выглядит пустой, потому что никакого содержимого элементу мы не добавили.
В теневом DOM стили добавляются непосредственно внутри его структуры в тег <style>
. Чтобы обратиться к теневому DOM, сначала нужно создать его для выбранного элемента через свойство Element.attachShadow()
, а затем через специальное свойство добавить стили.
Ищем наш элемент и прописываем режим отображения — всё это делаем в блоке <script>
:
// Получаем элемент с id "shadow-element"
const shadowElement = document.getElementById('shadow-element');
// Создаём теневой корень внутри элемента и задаём режим 'open' (открытый доступ к теневому DOM)
const shadowRoot = shadowElement.attachShadow({ mode: 'open' });
Теперь в теневом корне нам нужно сделать разметку и стили. Там же в скрипте добавим один абзац с помощью тега вставки HTML-кода <p>
:
shadowRoot.innerHTML = `
<style>
/* Стили для элемента <p> внутри теневого DOM */
p {
/* Размер шрифта */
font-size: 100px;
/* Цвет текста */
color: blue;
}
</style>
<!-- Сам элемент <p>, который будет виден на странице -->
<p>Нажми на меня</p>
`;
Сохраняем страницу, обновляем её в браузере и видим, что у нас появился контент:
Элемент добавлен на страницу и в инспекторе помечен как #shadow-root. Можно раскрыть его стили и посмотреть их. При этом элемента <p>
в обычной DOM-разметке нет. Если мы попытаемся обратиться к нему через JS и что-то с ним сделать, то получим ошибку:
// Находим первый элемент
на странице
const paragraph = document.querySelector('p');
// Меняем цвет текста на красный
paragraph.style.color = 'red';
Ошибка сообщает, что элемента <p>
на странице нет: вроде он есть и его как бы нет. Когда мы создаём теневой корень и кладём туда что-то, это значит, что все дальнейшие манипуляции с этим элементом нужно производить только в теневом корне.
Добавим нашему абзацу обработчик событий: при нажатии на него будет появляться окно с текстом. Делаем это там же в скрипте на странице, где у нас происходит вся остальная магия:
// Получаем элемент <p> из теневого DOM
const paragraphAct = shadowRoot.querySelector('p');
// Добавляем обработчик события для элемента <p>
paragraphAct.addEventListener('click', () => {
alert('Текст внутри теневого DOM 🌚');
});
Вот так мы добавили собственные скрытые стили и логику элементу, которого нет в обычном DOM-дереве.
Поскольку мы указали режим open
при создании элемента, это значит, что к нему можно получить доступ извне через element.shadowRoot
. Если мы поменяем режим теневого DOM на closed
, элемент не пропадёт — он всё равно будет отображаться на странице и будет доступен для пользователя. А вот программно к нему обратиться уже будет нельзя.
Проверим это через команду console.log(shadowElement.shadowRoot)
. В открытом режиме мы получим в консоли элемент и его стили, а в закрытом null
.
Библиотеки и фреймворки для работы с Shadow DOM
Обычно вручную теневой DOM прописывают редко. Для создания независимых компонентов разработчики используют библиотеки и фреймворки — React, Vue, Angular. При этом в самих этих фреймворках теневой DOM тоже не используют. Там есть другие механизмы для создания независимых компонентов: CSS-модули, специальные директивы (scoped в Vue) или механизмы эмуляции инкапсуляции (ViewEncapsulation в Angular).Но есть инструменты, где активно используется теневой DOM для изоляции стилей и логики. Например, библиотеки Lit, Stencil и Shoelace.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Теневой DOM</title>
</head>
<body>
<!-- Контейнер для теневого DOM -->
<div id="shadow-element"></div>
<script>
// Получаем элемент с id "shadow-element"
const shadowElement = document.getElementById('shadow-element');
// Создаём теневой корень внутри элемента и задаём режим 'open' (открытый доступ к теневому DOM)
const shadowRoot = shadowElement.attachShadow({ mode: 'closed' });
// Добавляем разметку и стили внутрь теневого DOM
shadowRoot.innerHTML = `
<style>
/* Стили для элемента <p> внутри теневого DOM */
p {
/* Размер шрифта */
font-size: 100px;
/* Цвет текста */
color: blue;
}
</style>
<!-- Сам элемент <p>, который будет виден на странице -->
<p>Нажми на меня</p>
`;
// Получаем элемент <p> из теневого DOM
const paragraphAct = shadowRoot.querySelector('p');
// Добавляем обработчик события для элемента <p>
paragraphAct.addEventListener('click', () => {
alert('Текст внутри теневого DOM 🌚');
});
</script>
</body>
</html>