Перейти к основному содержимому

Web Components — Custom Elements и Shadow DOM

Разработчику Архитектору

Web Components - состав стандарта

Web Components — набор браузерных стандартов для переиспользуемых UI-блоков без обязательного React/Vue:

  1. Custom Elements — свои теги (<user-card>).
  2. Shadow DOM — изолированное поддерево DOM и стили внутри компонента.
  3. HTML templates<template> для клонирования разметки.
  4. Slots — точки вставки внешнего контента в шаблон.

Фреймворки (React, Vue, Angular) часто используют Shadow DOM выборочно или эмулируют инкапсуляцию по-своему. Нативные компоненты уместны в дизайн-системах, embed-виджетах и постепенной модернизации статичных страниц.

Предварительно: классы, работа с DOM, события.


Когда выбирать Web Components

Имеет смысл, если один UI-блок нужен в разных стеках — статический сайт, SPA, CMS, embed у партнёра. Нативные Custom Elements + Shadow DOM дают долгоживущий контракт без привязки к React/Vue; фреймворки при этом могут использовать Shadow DOM выборочно или эмулировать инкапсуляцию своим способом.


Custom Elements

Класс наследует HTMLElement, регистрируется один раз:

Код ITЗагрузка примера кода…

Разбор:

  • В этом фрагменте используется конструкция class UserCard extends HTMLElement { как точка входа сценария.
  • Код показывает последовательность действий: получение данных, проверка условий и выполнение целевого действия.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.
ХукКогда вызывается
connectedCallbackэлемент добавлен в документ
disconnectedCallbackудалён из документа
attributeChangedCallbackизменился атрибут из observedAttributes
adoptedCallbackэлемент перенесли в другой документ (редко)

Использование в HTML:

<user-card name="Анна">
<p>Менеджер проекта</p>
</user-card>

Разбор:

  • В этом фрагменте используется конструкция <user-card name="Анна"> как точка входа сценария.
  • Разметка задаёт опорные элементы интерфейса, к которым затем подключается JavaScript-логика.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.

Имена custom elements обязаны содержать дефис (user-card), чтобы не пересечься со встроенными тегами.


Shadow DOM

attachShadow() создаёт отдельное дерево, стили снаружи не проникают внутрь (и наоборот — с оговорками для CSS-переменных).

Код ITЗагрузка примера кода…

Разбор:

  • В этом фрагменте используется конструкция class StatusBadge extends HTMLElement { как точка входа сценария.
  • Код показывает последовательность действий: получение данных, проверка условий и выполнение целевого действия.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.
modeПоведение
openelement.shadowRoot доступен снаружи
closedshadowRoot снаружи null (как у встроенного <video>)

:host — стили самого хост-элемента. :host([active]) — при атрибуте active.


Слоты

<slot name="..."> — место, куда попадает разметка светлого DOM (дети custom element):

<article-panel>
<h2 slot="title">Новости</h2>
<p>Текст блока</p>
</article-panel>

Разбор:

  • В этом фрагменте используется конструкция <article-panel> как точка входа сценария.
  • Разметка задаёт опорные элементы интерфейса, к которым затем подключается JavaScript-логика.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.
// в shadow:
// <header><slot name="title"></slot></header>
// <div class="body"><slot></slot></div> <!-- default slot -->

Разбор:

  • В этом фрагменте используется конструкция // в shadow: как точка входа сценария.
  • Код показывает последовательность действий: получение данных, проверка условий и выполнение целевого действия.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.

События со слотов всплывают с пометкой, что прошли через shadow boundary — учитывайте при делегировании.


Шаблон template

Разметку удобно хранить в <template> и клонировать:

<template id="todo-item-tpl">
<li><button type="button" class="done"></button> <span class="text"></span></li>
</template>

Разбор:

  • В этом фрагменте используется конструкция <template id="todo-item-tpl"> как точка входа сценария.
  • Разметка задаёт опорные элементы интерфейса, к которым затем подключается JavaScript-логика.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.
const tpl = document.getElementById('todo-item-tpl');
const node = tpl.content.cloneNode(true);
node.querySelector('.text').textContent = 'Купить молоко';
list.appendChild(node);

Разбор:

  • В этом фрагменте используется конструкция const tpl = document.getElementById('todo-item-tpl'); как точка входа сценария.
  • Код показывает последовательность действий: получение данных, проверка условий и выполнение целевого действия.
  • Поиск элементов через querySelector явно привязывает сценарий к нужным узлам DOM.
  • Ключевые вызовы и свойства опираются на стандартные API JavaScript/браузера, поэтому шаблон легко перенести в реальный проект.
  • Такой пример удобно расширять обработкой ошибок, логированием и дополнительной валидацией входных данных.

В связке с Shadow DOM шаблон кладут внутрь shadow root при инициализации.


События и публичный API

  • Внутри класса вызывайте this.dispatchEvent(new CustomEvent('save', { detail: { id: 1 }, bubbles: true })) — слушатели на странице поймают событие при bubbles: true.
  • Публичные методы на классе (show(), reset()) вызывают снаружи: document.querySelector('user-card').focus().

Не экспонируйте внутренние узлы shadow root в продакшене — это ломает инкапсуляцию.


Когда брать Web Components, когда фреймворк

СитуацияПодход
Виджет на чужой CMS, один тег на страницеCustom Element + Shadow
Большое SPA, команда на ReactReact; web components для изолированных legacy-блоков
Дизайн-система для нескольких стековStencil/Lit → web components
Простой сайт без сборкиВанильный JS (виджеты) может быть проще

Минусы нативных компонентов — нет встроенного виртуального DOM, сложнее тестировать без браузера, SSR требует отдельных решений.


Практический маршрут внедрения

Если команда только начинает использовать Web Components, удобен поэтапный план:

  1. Выделить один изолированный UI-блок (badge, card, date-picker).
  2. Описать публичный API компонента через атрибуты, свойства и кастомные события.
  3. Сделать shadow-стили и проверить совместимость в текущей теме сайта.
  4. Подключить компонент в 2-3 разных страницах или сервисах.
  5. Зафиксировать контракт в документации дизайн-системы.

Так формируется стабильная библиотека компонентов, которую используют проекты на разных стеках.


Частые ошибки интеграции

  • Компонент напрямую манипулирует глобальным CSS и ломает внешние страницы.
  • Внутренние DOM-узлы возвращаются наружу как часть API, и инкапсуляция теряет смысл.
  • События не помечаются bubbles: true, из-за чего родительские контейнеры их не получают.
  • Логика рендера выполняется при каждом attributeChangedCallback без проверки изменений, что создаёт лишние перерисовки.

Для сценариев с формами и сетью сочетайте web components с валидацией и отменой запросов.


Краткий итог

Custom Elements дают свой тег и жизненный цикл. Shadow DOM изолирует разметку и CSS. Slots подключают внешний контент. Стандарты дополняют, а не обязаны заменять фреймворки — но полезны там, где нужен один переиспользуемый блок в любом окружении.