Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке shadow DOM у веб-розробці». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Shadow DOM** - це браузерний API, який приєднує окреме ізольоване DOM-дерево до елемента і тримає його CSS та HTML окремо від решти сторінки. ```js const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>p { color: red; }</style> <p>Цей параграф ігнорує всі зовнішні стилі сторінки.</p> `; ``` **Ключове:** CSS не може перетнути межу shadow в жодному напрямку. Це основа ізоляції стилів у Web Components.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Shadow DOM** - це браузерний API, який приєднує окреме ізольоване DOM-дерево до хост-елемента і тримає його HTML, CSS та JS окремо від решти сторінки. ## Теорія ### TL;DR - Як звуконепроникна квартира в гучному будинку: внутрішні стилі залишаються всередині, зовнішні не проникають. - Звичайний DOM змішує всі CSS-правила глобально; Shadow DOM проводить жорстку межу на рівні елемента. - Браузер виділяє окреме дерево вузлів для кожного shadow root; CSS-селектори зупиняються на цій межі і не перетинають її. - `mode: 'open'` відкриває shadow root для зовнішнього JS; `mode: 'closed'` ховає його, повертаючи `null` для `host.shadowRoot`. - Використовуй для повторно використовуваних UI-компонентів, сторонніх вбудовок або мікрофронтендів, де конфлікти стилів - реальна проблема. ### Швидкий приклад ```html <!DOCTYPE html> <html> <head> <style>p { color: blue !important; }</style> </head> <body> <p>Звичайний параграф (синій, під впливом CSS сторінки).</p> <div id="host"></div> <script> const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>p { color: red; }</style> <p>Shadow-параграф (червоний, CSS сторінки не має жодного ефекту).</p> `; </script> </body> </html> ``` Зовнішнє правило `!important` не може дістатися всередину shadow root. Червоний параграф ігнорує його повністю. Ось і вся суть. ### Головна різниця Без Shadow DOM весь CSS конкурує глобально через наслідування і специфічність. Один невдало названий клас `.btn` у сторонньому віджеті може перекрити твої стилі кнопки. Shadow DOM проводить жорстку межу: браузер рендерить окреме дерево, де внутрішні стилі відповідають лише внутрішнім елементам, а зовнішні не проникають. Єдиний місток - `host.shadowRoot`, і тільки коли `mode` є `'open'`. ### Коли використовувати - Повторно використовувані UI-віджети (dropdown-меню, модальні вікна, date picker), що постачаються з власними стилями. - Сторонні вбудовки (чат-віджети, кнопки оплати), які запускаються на невідомих хост-сторінках з непередбачуваними таблицями стилів. - Мікрофронтенди, де кілька команд ділять одну сторінку. Без Shadow DOM дві команди, що випадково назвали клас `.card` по-різному, будуть конфліктувати непомітно. Такі баги важко відстежити до першопричини. - Прості статичні сторінки або застосунки з повним контролем над CSS - Shadow DOM там не потрібен. Звичайний DOM простіший. ### Відкритий і закритий режим `{ mode: 'open' }` відкриває `host.shadowRoot` для зовнішнього JS. Більшість кастомних елементів і фреймворків використовують саме його. `{ mode: 'closed' }` змушує `host.shadowRoot` повертати `null`, блокуючи зовнішні запити. Саме так реалізовані панелі Chrome DevTools. Закритий режим не є межею безпеки. Браузерні DevTools все одно бачать дерево. Він лише запобігає випадковому зовнішньому доступу зі скриптів. ### Слоти і проекція light DOM `<slot>` всередині shadow root - це плейсхолдер для дочірніх елементів хоста. Ці елементи (light DOM, тобто зовнішній вміст) проектуються туди під час рендерингу. ```html <div id="host"> <span>Я light DOM</span> </div> <script> const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>span { color: green; font-weight: bold; }</style> <slot></slot> `; </script> ``` `<span>` рендериться зеленим і жирним, бо прослотований light DOM успадковує shadow-стилі. Якщо потрібно контролювати це явно - використовуй `::slotted(span)`. ### Як браузер це обробляє Браузер виділяє окремий вузол `ShadowRoot`, прив'язаний до хост-елемента. Під час рендерингу рушій компонує дочірні елементи light DOM у `<slot>` тіньового дерева, але пропускає CSS-резолюцію через межу. `document.querySelector('p')` не знайде параграфи всередині shadow root. Зовнішній JS-доступ можливий лише при `mode: 'open'` через `host.shadowRoot.querySelector('p')`. ### Типові помилки 1. **Звернення до вмісту shadow в закритому режимі** ```js const el = document.createElement('div'); document.body.appendChild(el); el.attachShadow({ mode: 'closed' }); // el.shadowRoot === null el.shadowRoot.querySelector('p'); // TypeError: Cannot read properties of null ``` Рішення: використовуй `{ mode: 'open' }`, якщо потрібен зовнішній доступ, або передавай посилання через власний API компонента. 2. **Очікування, що зовнішній CSS проникне всередину** ```css /* Цей селектор не може перетнути межу shadow */ my-element * { color: black !important; } ``` Для тематизації shadow-компонентів ззовні використовуй CSS custom properties (вони перетинають межу навмисно) або `::part()` для відкритих частин. 3. **Забуття, що проекція слота застосовує shadow-стилі** ```html <my-el><p>Зовнішній дочірній елемент</p></my-el> ``` Якщо в `<my-el>` є `<slot>`, той `<p>` рендериться всередині тіні та підхоплює shadow CSS. Явно пиши `::slotted(p) { color: inherit; }`, щоб контролювати цю поведінку. 4. **Припущення, що вкладені shadow root повністю ізольовані один від одного** Внутрішні shadow-стилі можуть взаємодіяти із зовнішнім тіньовим деревом через слот-композицію. Тестуй рендеринг явно. Подвійна тінь не означає подвійну ізоляцію автоматично. ### Реальні приклади використання - **Lit (Google)**: кожен компонент використовує Shadow DOM за замовчуванням, забезпечуючи нульові CSS-конфлікти. - **Angular Elements**: обертає Angular-компоненти в Shadow DOM для вбудовування в не-Angular хост-сторінки. - **Chrome DevTools**: кожна панель працює у закритому shadow root, щоб JS і CSS сторінки не зламали UI інструментів. - **Vaadin**: корпоративні компоненти таблиць і форм постачають узгоджені стилі через Shadow DOM у будь-якому хост-застосунку. ### Питання на співбесіді **Q:** У чому різниця між `open` і `closed` режимом? **A:** `open` відкриває `host.shadowRoot` для зовнішнього JS. `closed` повертає там `null`. Жоден з режимів не заважає браузерним DevTools перевіряти дерево. **Q:** Як CSS custom properties працюють через межу shadow? **A:** Вони перетинають межу навмисно. Хост-сторінка задає `--primary-color: red`, і shadow-стилі зчитують його. Це стандартний спосіб тематизації shadow DOM компонентів без порушення ізоляції. **Q:** Як працюють слоти і що таке проекція light DOM? **A:** `<slot>` - це плейсхолдер для дочірніх елементів хоста. Безіменний слот бере всіх прямих дітей. Іменовані слоти використовують `slot="name"` на дочірньому елементі і `<slot name="name">` в тіньовому шаблоні. **Q:** Чи впливає Shadow DOM на доступність? **A:** ARIA на хост-елементі поширюється коректно. Елементи всередині shadow-дерева потребують явних ролей і підписів. Скрінрідери обходять сплощене (composed) дерево, а не тіньове дерево окремо. **Q:** (Senior) Як відкрити shadow-внутрішності для зовнішньої тематизації без повного відкриття shadow root? **A:** Використовуй `::part()`. Позначай внутрішні елементи атрибутом `part="button"` у shadow-шаблоні. Хост-сторінки стилізують їх через `my-el::part(button) { background: var(--theme-color); }`. Це явне, контрольоване відкриття без витоку внутрішньої структури. ## Приклади ### Ізоляція стилів у 10 рядках ```html <!DOCTYPE html> <html> <head> <style>p { color: blue; font-size: 24px; }</style> </head> <body> <p>Звичайний параграф (синій, 24px).</p> <div id="host"></div> <script> const shadow = document.getElementById('host').attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>p { color: red; font-size: 14px; }</style> <p>Shadow-параграф (червоний, 14px). CSS сторінки його не торкається.</p> `; </script> </body> </html> ``` Два параграфи, два абсолютно незалежних CSS-контексти. Без `!important`. Без ігор із назвами класів. ### Todo-елемент як кастомний компонент ```js class TodoItem extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> .todo { padding: 1rem; border: 1px solid #ccc; border-radius: 4px; } .done { text-decoration: line-through; opacity: 0.5; } </style> <div class="todo"> <input type="checkbox"> <span class="text"></span> </div> `; const text = shadow.querySelector('.text'); const checkbox = shadow.querySelector('input'); checkbox.addEventListener('change', () => { text.classList.toggle('done', checkbox.checked); }); } connectedCallback() { this.shadowRoot.querySelector('.text').textContent = this.getAttribute('todo') || 'Порожній todo'; } } customElements.define('todo-item', TodoItem); ``` ```html <todo-item todo="Купити молоко"></todo-item> <todo-item todo="Написати тести"></todo-item> ``` Кожен `<todo-item>` працює у власному shadow root. CSS хост-сторінки не може перекрити закреслення. Ніяких спільних імен класів, ніяких конфліктів між компонентами. ### Тематизація через CSS custom properties і ::part() ```js class FancyButton extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).innerHTML = ` <style> button { background: var(--btn-bg, #0070f3); /* fallback - синій */ color: var(--btn-color, white); padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; } </style> <button part="btn"><slot></slot></button> `; } } customElements.define('fancy-button', FancyButton); ``` Тематизація з хост-сторінки: ```css /* CSS custom properties перетинають межу shadow */ fancy-button { --btn-bg: #e53e3e; } /* ::part() явно таргетує відкриті частини */ fancy-button::part(btn) { border-radius: 0; } ``` Саме так виробничі бібліотеки компонентів вирішують тематизацію без відкриття повних shadow-внутрішностей. Автор компонента сам вирішує, що є публічним (`part="btn"`), а решта залишається прихованою.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.