Web Components: кастомные элементы и Shadow DOM
Продвинутое3 минРедакция Innovialabs
Коротко
Web Components — набор стандартов браузера для создания собственных HTML-тегов с изолированными стилями; они хороши для дизайн-систем и виджетов, которые должны работать в любом фреймворке.
Что такое Web Components
Web Components — это не библиотека, а набор встроенных в браузер стандартов, которые позволяют создать собственный HTML-тег вроде <price-tag> и использовать его как обычный элемент. Никакого фреймворка для работы не нужно.
Набор состоит из трех частей:
- Custom Elements — регистрация своего тега и его поведения через JavaScript-класс.
- Shadow DOM — изолированное поддерево с собственными стилями, которые не протекают наружу и не ломаются от глобального CSS.
- Templates и slots — заготовки разметки и «окна», куда пользователь компонента вставляет свой контент.
Custom Elements: свой тег за несколько строк
Элемент описывается классом, который наследуется от HTMLElement, и регистрируется через customElements.define. Имя обязательно должно содержать дефис.
class PriceTag extends HTMLElement {
static observedAttributes = ["amount"];
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const amount = Number(this.getAttribute("amount") || 0);
this.textContent = amount.toLocaleString("ru-RU") + " сум";
}
}
customElements.define("price-tag", PriceTag);
Основные хуки жизненного цикла:
connectedCallback— элемент добавлен в документ;disconnectedCallback— удален (здесь снимают слушатели и таймеры);attributeChangedCallback— изменился атрибут из спискаobservedAttributes.
Shadow DOM: изоляция стилей
Shadow DOM создает внутри элемента отдельное дерево. Стили внутри него не влияют на страницу, а стили страницы почти не влияют на него.
class UiCard extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
:host { display: block; border-radius: 12px; padding: 16px; }
::slotted(h3) { margin: 0; }
</style>
<slot name="title"></slot>
<slot></slot>
`;
}
}
customElements.define("ui-card", UiCard);
Что важно знать на практике:
- Наследуемые свойства (
color,font-family) проходят внутрь — это удобно для темы. - CSS custom properties (
--brand-color) тоже проходят сквозь границу — это основной способ кастомизации. - Для точечной стилизации снаружи есть
::part()— если автор компонента пометил элемент атрибутомpart.
Templates и slots
Тег <template> хранит разметку, которая не рендерится, пока ее не клонируют. Слоты определяют, куда попадет контент, переданный внутрь компонента:
<ui-card>
<h3 slot="title">Тариф «Старт»</h3>
<p>Описание тарифа</p>
</ui-card>
Заголовок попадет в именованный слот title, остальное — в слот по умолчанию.
Совместимость с фреймворками
Web Components работают везде, где есть DOM, но есть нюансы:
| Аспект | Что учитывать |
|---|---|
| Атрибуты | Всегда строки; сложные данные лучше передавать через свойства |
| События | Компонент отправляет CustomEvent, фреймворк подписывается на него |
| React | Современные версии лучше передают свойства и события в custom elements; в старых нужны обертки |
| Vue, Angular, Svelte | Поддерживают custom elements хорошо, иногда нужно явно указать, какие теги кастомные |
| SSR | Shadow DOM рендерится на клиенте; для сервера есть Declarative Shadow DOM, но экосистема еще развивается |
Для событий наружу используйте bubbles: true и composed: true, иначе событие не выйдет за границу Shadow DOM.
Когда Web Components имеют смысл
Подходят, если:
- нужна дизайн-система, которой пользуются команды на разных фреймворках;
- вы делаете встраиваемый виджет (чат, форма, калькулятор) для чужих сайтов;
- нужен долгий срок жизни компонентов без привязки к версии фреймворка;
- проект — сайт на CMS или серверных шаблонах, где тяжелый фреймворк избыточен.
Не лучший выбор, если все приложение уже на одном фреймворке: его собственные компоненты дают удобнее состояние, роутинг и SSR. Для сложных интерфейсов чаще берут легкие библиотеки поверх стандарта, например Lit, — они убирают ручной рендеринг.
Частые ошибки
- Тяжелая работа в
constructorвместоconnectedCallback. - Передача объектов через атрибуты с
JSON.stringify. - Забытая очистка слушателей в
disconnectedCallback. - Отсутствие доступности: у кастомной кнопки нет роли, фокуса и обработки клавиатуры.
- Повторный вызов
customElements.defineс тем же именем — это вызывает ошибку.