Shadow DOM — это стандарт платформы Web Components, обеспечивающий жёсткую инкапсуляцию структуры, стилей и поведения компонента. Lit строится поверх нативных Web Components и напрямую использует Shadow DOM как фундамент для создания изолированных, переиспользуемых UI-элементов.
Каждый компонент Lit по умолчанию рендерится в собственное shadow-дерево, отделённое от основного DOM документа. Это означает, что HTML-разметка и CSS внутри компонента не конфликтуют с глобальной страницей и другими компонентами.
import { LitElement, html } from 'lit';
class MyElement extends LitElement {
render() {
return html`<p>Текст внутри Shadow DOM</p>`;
}
}
customElements.define('my-element', MyElement);
При вставке <my-element></my-element> в
документ создаётся shadow root, связанный с этим элементом.
Shadow DOM формирует отдельное дерево узлов, недоступное для прямого обхода через стандартные DOM-селекторы.
document.querySelector('p'); // не найдёт <p> внутри shadow root
Доступ возможен только через shadowRoot конкретного
элемента:
const el = document.querySelector('my-element');
el.shadowRoot.querySelector('p');
Это свойство решает ряд архитектурных проблем:
В Lit стили объявляются через статическое поле styles и
применяются только к shadow-дереву компонента.
import { css, LitElement, html } from 'lit';
class StyledElement extends LitElement {
static styles = css`
p {
color: red;
font-weight: bold;
}
`;
render() {
return html`<p>Красный текст</p>`;
}
}
CSS из этого блока:
Это принципиально отличается от классического подхода с BEM, CSS Modules или scoped-CSS в сборщиках.
Глобальные стили, объявленные в <style> документа
или внешних CSS-файлах, не проникают внутрь Shadow
DOM:
p {
color: green;
}
Этот стиль не изменит <p> внутри компонента Lit.
Такое поведение исключает визуальные баги, возникающие при
масштабировании проекта.
Полная изоляция сопровождается особыми правилами наследования:
Некоторые CSS-свойства всё же наследуются:
colorfont-familyfont-sizeline-heightЕсли они не переопределены внутри компонента, значения берутся из
хоста (:host).
:host:host — основной механизм стилизации корневого элемента
компонента.
:host {
display: block;
padding: 8px;
background: #f5f5f5;
}
Допустимы модификации:
:host([disabled]) {
opacity: 0.5;
}
:host(.active) {
border: 1px solid blue;
}
Таким образом реализуется связь между внешними атрибутами и внутренним оформлением без утечки стилей.
:host-context()Позволяет реагировать на контекст, в котором находится компонент.
:host-context(.dark-theme) {
background: #222;
color: #fff;
}
Этот селектор проверяет окружение вне Shadow DOM, но применяет стиль внутри компонента. Используется ограниченно и требует осторожности, так как снижает изоляцию.
display: contentsПо умолчанию кастомный элемент — это обычный DOM-узел. Иногда требуется, чтобы он не влиял на layout.
:host {
display: contents;
}
В этом случае элемент исчезает из визуального дерева, а его содержимое участвует в потоке документа напрямую. Это полезно для логических компонентов-обёрток, но имеет ограничения:
Shadow DOM блокирует прямое влияние CSS, но CSS Custom Properties проходят сквозь границу.
my-element {
--main-color: purple;
}
static styles = css`
p {
color: var(--main-color, black);
}
`;
Преимущества такого подхода:
CSS-переменные — основной рекомендованный способ кастомизации компонентов Lit извне.
Полная изоляция делает компонент закрытым. Для передачи
пользовательского HTML используется механизм
<slot>.
render() {
return html`
<div class="container">
<slot></slot>
</div>
`;
}
Контент, переданный между тегами компонента, рендерится внутри shadow-дерева, но логически остаётся во внешнем DOM.
Для этого применяется селектор ::slotted():
::slotted(p) {
margin: 0;
color: blue;
}
Ограничения:
Это сознательное ограничение, поддерживающее инкапсуляцию.
По умолчанию события всплывают через границу Shadow DOM, но их
target перенаправляется.
this.dispatchEvent(new CustomEvent('my-event', {
detail: { value: 42 },
bubbles: true,
composed: true
}));
Флаг composed: true разрешает выход события за пределы
shadow-дерева. Без него событие будет остановлено на границе
компонента.
Внешний код видит событие так, будто оно произошло на хост-элементе, а не на внутреннем DOM-узле. Это предотвращает зависимость от внутренней структуры компонента.
Lit позволяет отказаться от Shadow DOM, переопределив
createRenderRoot:
createRenderRoot() {
return this;
}
В этом случае:
Такой режим используется редко и только при интеграции с legacy-CSS или сторонними библиотеками, не совместимыми с Shadow DOM.
Shadow DOM не создаёт ощутимой нагрузки на производительность:
При большом количестве компонентов выгода от изоляции превышает накладные расходы. Архитектура остаётся устойчивой при росте кодовой базы.
Использование Shadow DOM в Lit формирует чёткую модель проектирования:
Это сближает фронтенд-разработку с принципами модульного и объектно-ориентированного проектирования, где интерфейс и реализация строго разделены.