W3docs

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

Стилизация веб-компонентов через Shadow DOM: :host, :host(), :host-context(), ::slotted(), CSS-переменные и adoptedStyleSheets.

Shadow DOM даёт компоненту собственное приватное DOM-дерево и собственные приватные стили. Эта страница посвящена стилизации: как CSS внутри shadow root инкапсулируется, какие специальные селекторы используются для обращения к хост-элементу и к слотированному содержимому (:host, :host(), :host-context(), ::slotted()), как разрешить внешнему CSS настраивать компонент намеренно (пользовательские свойства) и двумя способами подключить таблицу стилей (<style> и adoptedStyleSheets).

Если Shadow DOM вам незнаком, сначала прочитайте JavaScript Shadow DOM, а раздел Web Components поможет понять более широкую картину того, где используются shadow roots.

Почему стили инкапсулируются

Главное обещание Shadow DOM — двусторонняя граница стилей:

  • Внешний CSS не проникает внутрь. Правило p { color: red }, заданное для всей страницы, не затронет <p> внутри shadow root. Именно это делает компоненты безопасными для использования на любой странице.
  • Внутренний CSS не вытекает наружу. Стили внутри shadow root применяются только в пределах этого root, поэтому можно использовать короткие, обобщённые селекторы (button, p, .title), не беспокоясь о конфликтах с хост-страницей.

Это отличается от обычной модели стилей и классов, где каждый селектор соперничает в одной глобальной области видимости. Внутри shadow root область видимости является областью по умолчанию.

Создание shadow root

Для начала прикрепите shadow root к хост-элементу. Всё, что вы поместите внутрь — разметка и CSS — будет инкапсулировано.

<body>
  <div id="my-element"></div>
    <script>
      // Creating Shadow DOM
      const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });
    
      // Styling Shadow DOM
      shadowRoot.innerHTML = `
        <p>A simple shadow root content.</p>
      `;
    </script>
</body>

Здесь мы прикрепляем shadow root методом attachShadow() и устанавливаем mode в 'open', что позволяет впоследствии получить root через element.shadowRoot. ('closed' скрывает его от внешних скриптов, но не добавляет реальной безопасности.)

Добавление ограниченных стилей с помощью <style>

Самый простой способ стилизовать shadow root — поместить внутрь элемент <style>. Такие правила применяются только в пределах root, а правила страницы остаются снаружи.

<div id="my-element">
  <!-- Shadow DOM content -->
</div>

<script>
  const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      /* Scoped styles */
      :host {
        display: block;
        border: 2px solid #333;
        padding: 10px;
      }

      p {
        color: blue;
      }
    </style>

    <p>This paragraph is styled within the Shadow DOM.</p>
  `;
</script>

Правило p { color: blue } окрашивает только абзац внутри этого root — <p> в другом месте страницы не затрагивается. Правило :host (ниже) стилизует сам хост-элемент.

Обращение к хосту: :host, :host(), :host-context()

Shadow root не может обращаться к своему хост-элементу с помощью обычного селектора, поскольку хост находится за пределами root. Три псевдокласса перекрывают этот пробел:

СелекторСовпадаетДля чего использовать
:hostХост-элемент, всегдаБазовые стили компонента (display, padding, блочная модель).
:host(<selector>)Хост только когда совпадает с <selector>Варианты и состояния, управляемые атрибутами/классами/псевдоклассами, например :host([disabled]), :host(:hover).
:host-context(<selector>)Хост, когда предок совпадает с <selector>Адаптация к контексту, например :host-context(.dark-theme).
<div class="dark-theme">
  <fancy-box disabled>Boxed content</fancy-box>
</div>

<script>
  class FancyBox extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host {
            display: block;
            padding: 12px;
            border: 2px solid #007bff;
          }
          /* Variant: applies only when the host has [disabled] */
          :host([disabled]) {
            opacity: 0.5;
            pointer-events: none;
          }
          /* Context: applies when any ancestor has .dark-theme */
          :host-context(.dark-theme) {
            background: #1e1e1e;
            color: #fff;
          }
        </style>
        <slot></slot>
      `;
    }
  }
  customElements.define('fancy-box', FancyBox);
</script>

Поскольку хост-элемент изначально имеет [disabled] и находится внутри .dark-theme, применяются все три правила: он отображается тёмным, приглушённым и неинтерактивным.

Внимание

:host-context() имеет ограниченную поддержку в браузерах (на момент написания Firefox не поддерживает). Для широкой совместимости лучше использовать CSS-переменную или явный атрибут на хосте.

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

Содержимое, которое пользователь передаёт в компонент, находится в light DOM и отображается через <slot>. Такое содержимое по-прежнему принадлежит странице, поэтому её собственные стили имеют приоритет — однако вы всё равно можете обратиться к нему изнутри shadow root с помощью ::slotted().

Важное ограничение: ::slotted() совпадает только с верхнеуровневыми слотированными узлами, но не с их потомками. ::slotted(span) работает; ::slotted(div span) — нет.

<body>
<script>
  class CustomButton extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: 'open' });

      shadowRoot.innerHTML = `
        <style>
          :host {
            display: inline-block;
            padding: 10px 20px;
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
          }

          :host(:hover) {
            background-color: #0056b3;
          }

          button {
            font-weight: bold;
            border: none;
            background: none;
            color: inherit;
            cursor: inherit;
            padding: 0;
          }

          /* Styling slotted content */
          ::slotted(span) {
            font-style: italic;
            text-decoration: underline;
          }
        </style>

        <button>
          <slot></slot>
        </button>
      `;
    }
  }

  customElements.define('custom-button', CustomButton);
</script>

<!-- Test custom-button with slotted content -->
<custom-button id="my-button">Click <span>here</span></custom-button>
</body>

Здесь ::slotted(span) нацелен на <span>, переданный как содержимое слота, делая его курсивным и подчёркнутым, тогда как окружающий текст "Click" остаётся нетронутым.

Разрешение странице настраивать компонент: CSS-переменные

Инкапсуляция — это отлично, но порой она ощущается как стена: хост-страница не может проникнуть внутрь, чтобы изменить цвет кнопки. Предусмотренный выход — CSS-переменные (пользовательские свойства) — единственное, что пронизывает shadow-границу. Компонент читает переменную через var() и задаёт запасное значение; страница устанавливает эту переменную снаружи.

<style>
  /* The page customizes the component from outside the boundary */
  theme-button {
    --btn-bg: #28a745;
    --btn-bg-hover: #1e7e34;
  }
</style>

<theme-button>Save</theme-button>

<script>
  class ThemeButton extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <style>
          :host {
            /* var(--name, fallback): fallback is used if the page sets nothing */
            background: var(--btn-bg, #007bff);
            color: #fff;
            padding: 8px 16px;
            display: inline-block;
            cursor: pointer;
          }
          :host(:hover) {
            background: var(--btn-bg-hover, #0056b3);
          }
        </style>
        <slot></slot>
      `;
    }
  }
  customElements.define('theme-button', ThemeButton);
</script>

Кнопка отображается зелёной, потому что страница задала --btn-bg. Удалите эти два объявления — и она вернётся к синему цвету (#007bff). Это самый чистый способ предоставить API темизации, сохраняя внутреннее устройство компонента приватным.

<style> или adoptedStyleSheets

Размещение тега <style> в innerHTML каждого экземпляра работает, но дублирует текст CSS для каждого компонента на странице и вынуждает браузер разбирать его заново. Для компонентов, создаваемых многократно, лучше разделить один разобранный CSSStyleSheet между несколькими roots с помощью adoptedStyleSheets.

<my-badge>New</my-badge>
<my-badge>Beta</my-badge>

<script>
  // Parsed once, reused by every instance
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
    :host {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 999px;
      background: #007bff;
      color: #fff;
      font-size: 12px;
    }
  `);

  class MyBadge extends HTMLElement {
    constructor() {
      super();
      const root = this.attachShadow({ mode: 'open' });
      root.adoptedStyleSheets = [sheet]; // adopt the shared sheet
      root.innerHTML = `<slot></slot>`;
    }
  }
  customElements.define('my-badge', MyBadge);
</script>

Когда что использовать:

  • <style> внутри root — простейший вариант, без лишнего кода, подходит для разовых компонентов или небольших демонстраций.
  • adoptedStyleSheets — предпочтителен, когда один и тот же компонент используется многократно: одна общая конструируемая таблица стилей означает меньший расход памяти и более быструю инициализацию. Кроме того, таблицу можно обновлять во время выполнения (sheet.replaceSync(...)), и все принявшие её roots немедленно отразят изменения.

Заключение

Стилизация Shadow DOM строится на нескольких идеях: стили инкапсулируются в обоих направлениях, к хосту обращаются через :host / :host() / :host-context(), к проецируемому содержимому — через ::slotted(), темизация реализуется с помощью CSS-переменных, а CSS подключается либо встроенно через <style>, либо эффективно через adoptedStyleSheets. В совокупности это позволяет создавать компоненты, которые выглядят правильно в любом контексте и при этом остаются настраиваемыми на ваших условиях.

Чтобы продвинуться дальше, изучите Shadow DOM Slots & Composition о том, как собирается слотированное содержимое, и Web Components о том, как сочетать shadow roots с пользовательскими элементами и шаблонами.

Практика

Практика
Какая возможность Shadow DOM позволяет разработчикам стилизовать содержимое, проецируемое в пользовательские элементы?
Какая возможность Shadow DOM позволяет разработчикам стилизовать содержимое, проецируемое в пользовательские элементы?
Was this page helpful?