What we do in the Shadow (DOM)

Алексей Назаренко, MinskCSS #12, 2024.

What we
do in the
Shadow
(DOM)

Алексей Назаренко

Алексей Назаренко

Алексей Назаренко.

Shadow DOM

План

Что такое Shadow DOM

Light DOM

<html lang="ru"> <head> <title>Лучшие кроссовки</title> </head> <body> <form action="/search" method="get" autocomplete="off"> <label for="search">Поиск:</label> <input type="search" id="search" name="q" placeholder="Кроссовки"> <button>Найти<button> </form> </body> </html>

Включение User Agent Shadow DOM

Панель настроек DevTools, выделен чекбокс 'Show user agent shadow DOM'.

User Agent Shadow DOM

<input type="search" id="search" name="q" placeholder="Кроссовки"> #shadow-root (user-agent) <div pseudo="-webkit-input-placeholder" id="placeholder" style="display: block !important;">Кроссовки</div> <div id="text-field-container" pseudo="-webkit-textfield-decoration-container" style="unicode-bidi: normal;"> <div id="editing-view-port"> <div></div> </div> <div pseudo="-webkit-search-cancel-button" id="search-clear" style="opacity: 0; pointer-events: none;"></div> </div> </input>

Shoelace Button

Скриншот страницы с кнопкой из документации библиотеки веб-компонентов Shoelace. Button

Авторский Shadow DOM

<sl-button variant="primary"> #shadow-root (open) <button part="base" class="button button--primary button--medium button--standard button--has-label" type="button" title name value role="button" aria-disabled="false" tabindex="0"> <slot name="prefix" part="prefix" class="button__prefix"></slot> <slot part="label" class="button__label">Button</slot> <slot name="suffix" part="suffix" class="button__suffix"></slot> </button> Button </sl-button>

Анатомия Shadow DOM

<sl-button variant="primary"> <!-- Shadow Host --> <!-- Shadow Boundary --> #shadow-root (open) <!-- Shadow Root --> <!-- Shadow Tree --> <button part="base" class="button button--primary button--medium button--standard button--has-label" type="button" title name value role="button" aria-disabled="false" tabindex="0"> <slot name="prefix" part="prefix" class="button__prefix"></slot> <slot part="label" class="button__label">Button</slot> <slot name="suffix" part="suffix" class="button__suffix"></slot> </button> <!-- Shadow Boundary --> Button </sl-button>

Зачем?

Инкапсуляция

Shadow DOM позволяет добавить к элементу поддерево DOM с изолированной* разметкой, стилями и поведением.

* Есть нюансы.

Как добавить?

Добавление Shadow DOM императивно

<div class="host"></div> const host = document.querySelector('.host'); host.attachShadow({ mode: 'open' }); const h1 = document.createElement('h1'); h1.textContent = 'Hello from shadow!'; host.shadowRoot.append(h1);

Добавление Shadow DOM декларативно

<div class="host"> <template shadowrootmode="open"> <h1>Hello from shadow!</h1> </template> </div> Declarative Shadow DOM

Declarative Shadow DOM Polyfill

(function attachShadowRoots(root) { root.querySelectorAll("template[shadowrootmode]").forEach(template => { const mode = template.getAttribute("shadowrootmode"); const shadowRoot = template.parentNode.attachShadow({ mode }); shadowRoot.appendChild(template.content); template.remove(); attachShadowRoots(shadowRoot); }); })(document); Declarative Shadow DOM Polyfill

Не только Custom Elements

Инкапсуляция
HTML

Инкапсуляция HTML

<form action="/subscribe" method="post"> <label for="email">Email:</label> <div class="field"> #shadow-root (open) <input type="email" id="email" aria-describedby="error" required> <svg id="error"><!-- ... --></svg> </div> <p id="error">Не заполнено поле email</p> <button>Подписаться</button> </form>

Отправка формы

<form action="/subscribe" method="post"> <!-- Данные из поля не отправятся --> <label for="email">Email:</label> <div class="field"> #shadow-root (open) <input type="email" id="email" aria-describedby="error" required> <svg id="error"><!-- ... --></svg> </div> <p id="error">Не заполнено поле email</p> <button>Подписаться</button> </form> More capable form controls

Валидация формы

<form action="/subscribe" method="post"> <label for="email">Email:</label> <div class="field"> #shadow-root (open) <input type="email" id="email" aria-describedby="error" required> <!-- Не валидируется --> <svg id="error"><!-- ... --></svg> </div> <p id="error">Не заполнено поле email</p> <button>Подписаться</button> </form> More capable form controls

ARIA-привязки

<form action="/subscribe" method="post"> <label for="email">Email:</label> <!-- Лейбл не привяжется --> <div class="field"> #shadow-root (open) <input type="email" id="email" aria-describedby="error" required> <svg id="error"><!-- ... --></svg> </div> <p id="error">Не заполнено поле email</p> <!-- Ошибка не привяжется --> <button>Подписаться</button> </form> Can I create an ARIA reference to an element in shadow DOM?

Дублирование id

<form action="/subscribe" method="post"> <label for="email">Email:</label> <div class="field"> #shadow-root (open) <input type="email" id="email" aria-describedby="error" required> <svg id="error"><!-- ... --></svg> <!-- id может повторяться --> </div> <p id="error">Не заполнено поле email</p> <!-- id может повторяться --> <button>Подписаться</button> </form>

Инкапсуляция
JS

Область видимости

<script> const foo = 'bar'; </script> <div class="field"> #shadow-root (open) <script> console.log(foo); // 'bar' console.log(window); // window </script> <input type="email" id="email" required> <svg id="error"><!-- ... --></svg> </div>

Поиск элементов

<script> console.log(document.querySelector('input')); // null </script> <div class="field"> #shadow-root (open) <script> console.log(document.querySelector('.field')); // null </script> <input type="email" id="email" required> <svg id="error"><!-- ... --></svg> </div>

Режимы Shadow DOM

mode: 'open'
<script> const field = document.querySelector('.field'); console.log(field.shadowRoot); // #shadow-root (open) console.log(field.shadowRoot.firstElementChild); // <input> </script> <div class="field"> #shadow-root (open) <input type="email" id="email" required> <svg id="error"><!-- ... --></svg> </div>

Режимы Shadow DOM

mode: 'closed'
<script> const field = document.querySelector('.field'); console.log(field.shadowRoot); // null console.log(field.shadowRoot.firstElementChild); // TypeError </script> <div class="field"> #shadow-root (closed) <input type="email" id="email" required> <svg id="error"><!-- ... --></svg> </div>

Инкапсуляция
CSS

Добавление стилей императивно

<div class="host"></div> const host = document.querySelector('.host'); host.attachShadow({ mode: 'open' }); host.shadowRoot.innerHTML = '<p>Shadow DOM</p>'; const stylesheet = new CSSStyleSheet(); stylesheet.replaceSync('p { color: red; }'); // CSS-in-JS! host.shadowRoot.adoptedStyleSheets.push(stylesheet); Constructable Stylesheets

Добавление стилей декларативно

<div class="host"> <template shadowrootmode="open"> <style> p { color: red; } </style> <style>@import url('/styles.css');</style> <link rel="stylesheet" href="/styles.css"> <p>Shadow DOM</p> </template> </div>

Внешние стили

<style> p { color: red; } </style> <p>Light DOM</p> <div> #shadow-root (open) <p>Shadow DOM</p> </div>

Внутренние стили

<p>Light DOM</p> <div> #shadow-root (open) <style> p { color: red; } </style> <p>Shadow DOM</p> </div>

Наследование

<style> .parent { color: red; } </style> <div class="parent"> <p>Light DOM</p> <div class="host"> #shadow-root (open) <p>Shadow DOM</p> </div> </div>

Наследуемые свойства

Стилизация хоста

<div class="host"> #shadow-root (open) <style> :host { border: 2px dashed red; } </style> <p>Shadow DOM</p> </div>
:host

Стилизация хоста

<style> .host { border: 2px dotted green; } </style> <div class="host"> #shadow-root (open) <style> :host { border: 2px dashed red; } </style> <p>Shadow DOM</p> </div>

Каскад и контекст

The CSS Cascade: A Deep Dive

Стилизация хоста

<div class="host green"> #shadow-root (open) <style> :host-context(.green) { border: 2px dashed green; } </style> <p>Shadow DOM</p> </div>
:host-context()

Custom Properties

<style> :root { --color: red; } </style> <div class="host"> #shadow-root (open) <style> p { color: var(--color); } </style> <p>Shadow DOM</p> </div>

Базовая стилизация: итоги

Shadow Parts

<style> .host::part(p) { border: 2px dashed green; } .host::part(p)::first-letter { color: red; } </style> <div class="host"> #shadow-root (open) <p part="p">Shadow DOM</p> </div>
::part()

Shadow Parts: комбинаторы

<style> .host::part(p) span { /* Так нельзя */ color: red; } </style> <div class="host"> #shadow-root (open) <p part="p"> Shadow <span>DOM<span> </p> </div>

Shadow Parts: несколько частей

<style> .host::part(s) { /* А так можно */ color: red; } </style> <div class="host"> #shadow-root (open) <p part="p"> Shadow <span part="s">DOM<span> </p> </div>

Shadow Parts: вложенные Shadow DOM

<style> .host::part(s) { /* Не работает */ color: red; } </style> <div class="host"> #shadow-root (open) <p> #shadow-root (open) Shadow  <span part="s">DOM<span> </p> </div>

Shadow Parts: экспорт частей

<style> .host::part(s) { /* Теперь работает */ color: red; } </style> <div class="host"> #shadow-root (open) <p exportparts="s"> <!-- экспорт --> #shadow-root (open) Shadow  <span part="s">DOM<span> </p> </div>

Shadow Parts: реэкспорт частей

<style> .host::part(span) { /* Другое имя */ color: red; } </style> <div class="host"> #shadow-root (open) <p exportparts="s:span"> #shadow-root (open) Shadow  <span part="s">DOM<span> </p> </div>

Shadow Parts: итоги

Slots: контент

<div class="host"> <template shadowrootmode="open"> <p>Shadow DOM</p> </template> <p>Light DOM</p> </div>

Slots: слот по умолчанию

<div class="host"> <template shadowrootmode="open"> <p>Shadow DOM</p> <slot></slot> </template> <p>Light DOM</p> </div>
The Web Component Slot element

Slots: именованные слоты

<div class="host"> <template shadowrootmode="open"> <slot name="before"></slot> <p>Shadow DOM</p> <slot></slot> </template> <p>Light DOM default</p> <p slot="before">Light DOM before</p> </div>
slot

Slots: внешние стили

<style> p { color: red; } </style> <div class="host"> <template shadowrootmode="open"> <p>Shadow DOM</p> <slot></slot> </template> <p>Light DOM</p> </div>

Slots: стилизация слотов

<div class="host"> <template shadowrootmode="open"> <style> ::slotted(*) { color: red; } </style> <p>Shadow DOM</p> <slot></slot> </template> <p>Light DOM</p> </div>
::slotted()

Slots: стилизация слотов

<div class="host"> <template shadowrootmode="open"> <style> ::slotted(div) { color: red; } </style> <p>Shadow DOM</p> <slot></slot> </template> <div>Light DOM div</div> <p>Light DOM p</p> </div>

Slots: внешние и внутренние стили

<style> p { color: red; } </style> <div class="host"> <template shadowrootmode="open"> <style> ::slotted(p) { color: green; } </style> <slot></slot> </template> <p>Light DOM span</p> </div>

Slots: итоги

Как быть?

API стилизации

shoelace.style

API стилизации в Shoelace

Customizing Shoelace

Ссылки

Не использовать Shadow DOM

Не использовать Shadow DOM

Ещё ссылки

Итоги

Спасибо за внимание!

Слайды: shadow-dom-minskcss.netlify.app
Канал: <divelopers>
Telegram: @alexnozer

Алексей Назаренко