Реалізація Shadow DOM для інкапсуляції стилів компонентів

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Shadow DOM для інкапсуляції стилів компонентів
Середня
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Shadow DOM для інкапсуляції стилів компонентів

Shadow DOM — механізм браузера для створення ізольованого поддерева DOM. Стилі зовні не проникають всередину, стилі зсередини не витікають назовні. Це дозволяє створювати компоненти, які працюють однаково у будь-якому CSS-оточенні.

Як працює Shadow DOM

Document (Light DOM)
├── <header>
├── <main>
│   └── <my-widget>      ← Custom Element
│       └── #shadow-root  ← Shadow Root (границя інкапсуляції)
│           ├── <style>   ← стилі видні тільки тут
│           └── <div class="widget">
│               └── <slot> ← проекція Light DOM контенту
└── <footer>

Глобальний CSS div { color: red } не впливає на div всередину shadow-root. CSS з shadow-root не впливає на div зовні.

Відкритий та закритий режими

// mode: 'open' — доступ до shadowRoot зовні (el.shadowRoot !== null)
const openShadow = element.attachShadow({ mode: 'open' })

// mode: 'closed' — shadowRoot === null зовні
// Використовується для максимальної інкапсуляції (нативні браузерні елементи)
const closedShadow = element.attachShadow({ mode: 'closed' })

У переважній більшості випадків використовується mode: 'open'—інструменти розробника, тестуючі утиліти та бібліотеки управління формами (react-hook-form, браузерні форми) потребують доступу до shadowRoot.

Стилізація зсередини

class StyledCard extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })

    shadow.innerHTML = `
      <style>
        /* :host — сам елемент <styled-card> */
        :host {
          display: block;
          border-radius: 12px;
          overflow: hidden;
        }

        /* :host() з умовою */
        :host([variant="dark"]) {
          background: #1a1a1a;
          color: #fff;
        }

        :host([variant="light"]) {
          background: #fff;
          color: #111;
          box-shadow: 0 2px 20px rgba(0,0,0,0.08);
        }

        /* :host-context() — реакція на оточення */
        :host-context(.dark-theme) {
          background: #222;
        }

        .card-body {
          padding: 24px;
        }

        .card-title {
          font-size: 20px;
          font-weight: 700;
          margin: 0 0 12px;
        }

        /* ::slotted — стилізація спроектованого контенту */
        ::slotted(p) {
          margin: 0;
          line-height: 1.6;
          opacity: 0.7;
        }

        ::slotted([slot="footer"]) {
          margin-top: 20px;
          padding-top: 16px;
          border-top: 1px solid currentColor;
          opacity: 0.2;
        }
      </style>

      <div class="card-body">
        <h3 class="card-title">
          <slot name="title">Заголовок</slot>
        </h3>
        <slot></slot>
        <slot name="footer"></slot>
      </div>
    `
  }
}

CSS Custom Properties: міст через Shadow DOM

Єдиний спосіб передати стилі всередину — через CSS-змінні. Вони проникають через Shadow DOM boundary.

/* Зовні: задаємо змінні */
styled-card {
  --card-bg: #f8f9fa;
  --card-radius: 16px;
  --card-padding: 32px;
  --card-title-color: #1a1a1a;
}
/* Всередину Shadow DOM: використовуємо змінні з fallback */
:host {
  background: var(--card-bg, #fff);
  border-radius: var(--card-radius, 8px);
}

.card-body {
  padding: var(--card-padding, 24px);
}

.card-title {
  color: var(--card-title-color, inherit);
}
<!-- Різні екземпляри з різними стилями -->
<styled-card style="--card-bg: #1a0050; --card-title-color: #fff">
  <span slot="title">Темна карточка</span>
  <p>Контент</p>
</styled-card>

<styled-card style="--card-bg: #e8f5e9; --card-padding: 40px">
  <span slot="title">Зелена карточка</span>
  <p>Контент</p>
</styled-card>

CSS Parts: точкова стилізація зовні

part атрибут відкриває конкретні елементи для зовнішніх стилів через ::part():

shadow.innerHTML = `
  <div class="wrapper" part="wrapper">
    <button class="btn" part="button trigger">
      <slot></slot>
    </button>
    <div class="dropdown" part="dropdown">
      <slot name="items"></slot>
    </div>
  </div>
`
/* Зовні — стилізуємо тільки відкриті частини */
my-dropdown::part(button) {
  background: #7000ff;
  color: #fff;
  border-radius: 8px;
}

my-dropdown::part(dropdown) {
  background: #1a1a1a;
  border: 1px solid #333;
}

/* hover на частину */
my-dropdown::part(button):hover {
  background: #5500cc;
}

Adoptable Stylesheets (CSSStyleSheet API)

Для переиспользования стилів між Shadow DOM без дублювання:

// Створюємо розділяємий stylesheet один раз
const sharedStyles = new CSSStyleSheet()
sharedStyles.replaceSync(`
  :host { box-sizing: border-box; }
  *, *::before, *::after { box-sizing: inherit; }

  button {
    cursor: pointer;
    border: none;
    font: inherit;
  }
`)

const typographyStyles = new CSSStyleSheet()
typographyStyles.replaceSync(`
  h1, h2, h3 { font-weight: 700; line-height: 1.2; margin: 0; }
  p { line-height: 1.6; margin: 0 0 1em; }
`)

// Застосовуємо в компонентах
class ComponentA extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    // Adoptable stylesheets — без дублювання CSS
    shadow.adoptedStyleSheets = [sharedStyles, typographyStyles]

    // Тільки компонент-специфічні стилі
    const localStyle = new CSSStyleSheet()
    localStyle.replaceSync(`.wrapper { background: blue; }`)
    shadow.adoptedStyleSheets.push(localStyle)
  }
}

class ComponentB extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    shadow.adoptedStyleSheets = [sharedStyles]  // тільки базові стилі
  }
}

Shadow DOM та форми

Нативні елементи форм всередину Shadow DOM не беруть участь в form.elements та не потрапляють в FormData. Рішення через ElementInternals:

class CustomInput extends HTMLElement {
  static get formAssociated() { return true }

  private internals: ElementInternals
  private shadow: ShadowRoot

  constructor() {
    super()
    // formAssociated + ElementInternals дозволяють брати участь у формі
    this.internals = this.attachInternals()
    this.shadow = this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    this.shadow.innerHTML = `
      <style>
        input {
          width: 100%;
          padding: 10px 14px;
          border: 1px solid var(--border-color, #d0d5dd);
          border-radius: 8px;
          font: inherit;
          outline: none;
        }
        input:focus {
          border-color: var(--focus-color, #7000ff);
          box-shadow: 0 0 0 3px var(--focus-ring, rgba(112, 0, 255, 0.15));
        }
      </style>
      <input type="text" />
    `

    const input = this.shadow.querySelector('input')!
    input.addEventListener('input', () => {
      // Синхронізуємо значення з формою
      this.internals.setFormValue(input.value)
      this.internals.setValidity(
        input.validity,
        input.validationMessage,
        input
      )
    })
  }

  // Браузер викликає при reset форми
  formResetCallback() {
    const input = this.shadow.querySelector('input')
    if (input) input.value = ''
    this.internals.setFormValue('')
  }
}

customElements.define('custom-input', CustomInput)

Терміни

Один компонент із Shadow DOM, Custom Properties та Parts — 1 день. Система з 5–8 компонентів із Adoptable Stylesheets, ElementInternals для форм та документацією — 1–2 тижні.