CSS в Shadow DOM

Shadow DOM создаёт инкапсулированный DOM-дерево внутри веб-компонента, отделяя его стили от глобальных. Это позволяет управлять внешним видом компонента без риска конфликта с другими стилями на странице. В библиотеке Haunted, которая использует Web Components и функциональный подход через хуки, понимание CSS в Shadow DOM критично для корректного оформления компонентов.

Подключение Shadow DOM

Haunted строится поверх стандартных веб-компонентов, поэтому для каждого кастомного элемента создаётся shadow root:

import { component, html, useState } from 'haunted';

function MyComponent() {
  const [count, setCount] = useState(0);

  return html`
    <style>
      button {
        background-color: #007acc;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 4px;
        cursor: pointer;
      }

      button:hover {
        background-color: #005fa3;
      }

      p {
        font-size: 16px;
        color: #333;
      }
    </style>
    <p>Счётчик: ${count}</p>
    <button @click=${() => setCount(count + 1)}>Увеличить</button>
  `;
}

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

Ключевой момент: <style> внутри шаблона Haunted автоматически помещается в shadow root и применяется только к этому компоненту. Это обеспечивает полную инкапсуляцию стилей.

Особенности работы CSS

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

  2. Использование CSS-переменных Shadow DOM позволяет передавать значения из глобального CSS через кастомные свойства (CSS variables):

:host {
  --button-bg: #007acc;
  --button-hover-bg: #005fa3;
}
button {
  background-color: var(--button-bg);
}
button:hover {
  background-color: var(--button-hover-bg);
}

:host ссылается на сам компонент, а не на вложенные элементы. Это облегчает создание настраиваемых компонентов, где цвета и отступы можно изменять снаружи через CSS-переменные.

  1. Селекторы :host и ::slotted

    • :host позволяет стилизовать сам элемент, а не его содержимое.
    • ::slotted(selector) применяется к элементам, переданным в слот:
function SlotComponent() {
  return html`
    <style>
      :host {
        display: block;
        padding: 12px;
        border: 1px solid #ccc;
      }
      ::slotted(p) {
        color: red;
      }
    </style>
    <slot></slot>
  `;
}

customElements.define('slot-component', component(SlotComponent));

В этом примере любой <p> внутри слота окрашивается в красный, при этом остальная структура компонента остаётся изолированной.

  1. Встроенные стили vs внешние CSS Haunted компоненты обычно используют инлайн <style> в шаблоне, так как это гарантирует попадание стилей в shadow root. Подключение внешних CSS-файлов возможно, но требует использования <link rel="stylesheet"> внутри shadow root или динамической вставки стилей через JavaScript.
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';
shadowRoot.appendChild(link);

Этот подход позволяет переиспользовать общие CSS-файлы, но при этом не нарушает инкапсуляцию.

  1. Анимации и переходы Анимации и CSS-трансформации работают внутри shadow root так же, как и в обычном DOM. Можно использовать keyframes и transition без ограничений, но они будут применяться только к элементам компонента:
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
p {
  animation: fadeIn 0.5s ease-in;
}

Практические рекомендации

  • Минимизировать глобальные зависимости: стили внутри shadow root должны быть самодостаточными.
  • Использовать CSS-переменные для настройки: это обеспечивает гибкость при интеграции компонента в разные проекты.
  • Оптимизировать размер стилей: инкапсуляция делает повторное определение одинаковых правил допустимым, но чрезмерное дублирование увеличивает размер страницы.
  • Стилизовать слоты через ::slotted: чтобы передаваемый контент гармонично сочетался с компонентом, не нарушая инкапсуляцию.

Взаимодействие с глобальными стилями

Хотя shadow DOM изолирует CSS, глобальные шрифты и медиа-запросы всё же действуют внутри компонента. Это означает, что можно наследовать семейство шрифтов или использовать @media внутри <style> Haunted-компонента:

@media (max-width: 600px) {
  p {
    font-size: 14px;
  }
}

Таким образом, компоненты остаются адаптивными без зависимости от внешней верстки.

Итоговая структура Haunted-компонента с CSS

function AdvancedComponent() {
  return html`
    <style>
      :host {
        display: block;
        padding: 16px;
        border-radius: 8px;
        background-color: #f9f9f9;
      }
      h2 {
        margin: 0 0 12px 0;
        color: #222;
      }
      button {
        background-color: var(--button-bg, #007acc);
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover {
        background-color: var(--button-hover-bg, #005fa3);
      }
    </style>
    <h2>Заголовок компонента</h2>
    <button @click=${() => console.log('Clicked')}>Кнопка</button>
  `;
}

customElements.define('advanced-component', component(AdvancedComponent));

Такой подход обеспечивает полностью изолированные, настраиваемые и адаптивные компоненты с полной поддержкой CSS внутри Shadow DOM.