Back

Стилизация веб-компонентов с помощью Shadow DOM и CSS

Стилизация веб-компонентов с помощью Shadow DOM и CSS

Если вы когда-либо размещали веб-компонент на странице и недоумевали, почему ваш глобальный CSS перестал работать внутри него, значит, вы столкнулись с границей теневого DOM. Это не баг — в этом и есть суть. Shadow DOM предоставляет пользовательским элементам собственное изолированное DOM-дерево, что означает: стили по умолчанию не проникают ни внутрь, ни наружу. Но это не значит, что стилизация невозможна. Это значит, что стилизация становится осознанной.

В этой статье рассматриваются основные механизмы стилизации Shadow DOM: внутренние стили, :host, ::slotted(), пользовательские свойства CSS, ::part() и adoptedStyleSheets.

Ключевые выводы

  • Shadow DOM изолирует стили по своей природе, поэтому глобальный CSS не может проникнуть внутрь компонента, а внутренние стили не могут просочиться наружу.
  • Теневые стили действуют как значения по умолчанию и располагаются ниже авторских стилей страницы в каскаде, подобно user-agent-таблице стилей браузера.
  • Используйте :host и :host() для стилизации самого элемента компонента, а ::slotted() — для стилизации спроецированных дочерних элементов из light DOM (только прямых потомков).
  • Пользовательские свойства CSS и ::part() формируют целенаправленный публичный API стилизации: переменные — для тем и токенов, части (parts) — для структурной стилизации.
  • adoptedStyleSheets позволяет нескольким компонентам совместно использовать единый распарсенный CSSStyleSheet, что повышает производительность при масштабировании.

Почему Shadow DOM меняет поведение CSS

Когда вы вызываете attachShadow() на элементе, вы создаёте отдельное DOM-дерево, прикреплённое к этому элементу (shadow host). Селекторы из таблицы стилей страницы не могут проникнуть внутрь этого дерева, а селекторы внутри теневого дерева не могут достучаться до внешнего мира.

Есть один важный нюанс, который стоит понимать: стили shadow DOM располагаются ниже ваших авторских стилей в каскаде CSS. Это означает, что глобальное правило вроде * { color: red } переопределит правило :host { color: green } внутри тени — даже если глобальное правило встречается раньше в исходном коде. Воспринимайте теневые стили как стили компонента по умолчанию — аналогично тому, как user-agent-таблица стилей браузера предоставляет значения по умолчанию для <button> или <input>.

Стилизация теневого хоста с помощью :host

Внутри теневого дерева :host выбирает элемент, к которому прикреплена тень:

:host {
  display: block;
  font-family: sans-serif;
}

Вы также можете использовать :host(selector), чтобы применять стили условно, в зависимости от атрибутов или классов хоста:

:host([disabled]) {
  opacity: 0.5;
  pointer-events: none;
}

Поскольку хост-элемент находится в light DOM, стили уровня документа переопределят правила :host. Если вы хотите, чтобы значения по умолчанию компонента имели приоритет, прибегайте к !important внутри тени — это один из редких случаев, когда такое использование действительно уместно.

Стилизация слотового контента с помощью ::slotted()

Слоты позволяют проецировать содержимое light DOM в теневое дерево. Псевдоэлемент ::slotted() позволяет стилизовать эти спроецированные элементы изнутри тени:

::slotted(p) {
  margin: 0;
  color: #333;
}

Важное ограничение: ::slotted() соответствует только элементу, непосредственно назначенному слоту, — но не его потомкам. Селектор вроде ::slotted(p span) работать не будет, и ::slotted() принимает только составной селектор (без комбинаторов потомков). Для более глубокой стилизации полагайтесь на наследование CSS или позвольте собственным стилям light DOM справиться с этим.

Пользовательские свойства CSS пересекают границу теневого DOM

CSS-переменные (пользовательские свойства) свободно пересекают границу теневого DOM. Это делает их самым гибким инструментом для тематизации веб-компонентов:

/* Внутри теневого дерева */
:host {
  background: var(--card-bg, white);
  color: var(--card-color, black);
}
/* В таблице стилей страницы */
my-card {
  --card-bg: #1a1a2e;
  --card-color: #eee;
}

Компонент определяет «крючки»; потребитель задаёт значения. Всегда указывайте резервные значения, чтобы компонент работал без какой-либо внешней конфигурации.

Открытие точек стилизации через CSS Shadow Parts (::part())

CSS Shadow Parts — это современный ответ на задачу стилизации внутренних элементов веб-компонента снаружи. Внутри компонента вы помечаете элементы атрибутом part:

<button part="trigger">Open</button>

Снаружи компонента потребители могут напрямую обращаться к этой части с помощью ::part():

my-dialog::part(trigger) {
  background: royalblue;
  border-radius: 4px;
}

Это намеренный API стилизации — автор компонента решает, что именно открывается наружу. Он мощнее CSS-переменных для структурной стилизации (расположение, границы, фон), при этом сохраняя внутренние детали реализации приватными.

Эффективное совместное использование стилей через adoptedStyleSheets

adoptedStyleSheets позволяет прикреплять объекты CSSStyleSheet непосредственно к теневому корню. Этот механизм имеет широкую поддержку в современных браузерах и хорошо подходит для библиотек компонентов, которым нужно делиться одной распарсенной таблицей стилей между множеством экземпляров:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.adoptedStyleSheets = [sheet];
  }
}

customElements.define('my-component', MyComponent);

Браузер парсит таблицу стилей один раз, независимо от того, сколько компонентов её используют, — это ощутимый выигрыш в производительности при масштабировании. Вы можете добавлять или удалять таблицы стилей, изменяя массив adoptedStyleSheets, а изменения в общем CSSStyleSheet применяются везде, где он был принят.

Выбор правильного подхода

ЦельИнструмент
Стилизовать хост-элемент компонента:host / :host()
Стилизовать спроецированный контент light DOM::slotted()
Предоставить токены тематизации потребителямCSS custom properties
Предоставить структурную стилизацию::part()
Делить стили между множеством компонентовadoptedStyleSheets

Заключение

Стилизация Shadow DOM не направлена на то, чтобы заблокировать CSS, — её цель сделать инкапсуляцию явной. Используйте внутренние стили и :host для значений по умолчанию, CSS-переменные и ::part() в качестве публичного API стилизации, а adoptedStyleSheets — когда важна производительность при масштабировании. Как только эта ментальная модель закрепится, стилизация веб-компонентов станет простой и понятной.

FAQ

Потому что компонент использует Shadow DOM, который создаёт изолированное DOM-дерево, отделённое от основного документа. Таблицы стилей страницы не могут выбирать элементы внутри теневого дерева. Чтобы стилизовать внутренности, автор компонента должен предоставить точки доступа — например, пользовательские свойства CSS или CSS Shadow Parts через атрибут part, к которым вы затем обращаетесь снаружи с помощью ::part().

Используйте пользовательские свойства для дизайн-токенов — цветов, отступов, шрифтов, — которые естественно распространяются через наследование. Используйте ::part(), когда потребителям нужен структурный контроль над конкретным внутренним элементом, например для переопределения границ, фона или расположения у кнопки или заголовка. Parts дают более тонкий доступ, тогда как переменные остаются проще и шире по охвату.

Псевдоэлемент ::slotted() соответствует только узлам верхнего уровня, напрямую назначенным слоту, а не их потомкам. Кроме того, он принимает только составной селектор, поэтому комбинаторы потомков недопустимы. Чтобы стилизовать дочерние элементы слотового содержимого, полагайтесь на наследование CSS от слотового элемента или позвольте таблице стилей light DOM потребителя обрабатывать эти потомки напрямую.

Да. Он поддерживается во всех современных evergreen-браузерах и является рекомендуемым способом совместного использования стилей в нескольких теневых корнях без повторного парсинга. Один распарсенный CSSStyleSheet можно прикрепить к множеству теневых корней, что снижает потребление памяти и улучшает время запуска. Общие таблицы стилей также можно эффективно обновлять и переиспользовать в нескольких компонентах.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay