Web Components (Custom Elements и Shadow DOM): практическое руководство для начинающих
Если вы хотите делать аккуратные и переиспользуемые интерфейсные блоки без React/Vue, Web Components — то, что нужно. В этом руководстве мы шаг за шагом создадим несколько компонентов, разберём Custom Elements, Shadow DOM, шаблоны, слоты, работу с атрибутами и событиями. Всё на чистом HTML5+JS.
Что такое Web Components
- Custom Elements — собственные HTML-теги, например
<user-card>. - Shadow DOM — инкапсуляция разметки и стилей внутри компонента.
- HTML Templates & Slots — шаблоны для разметки и точки вставки пользовательского контента.
Поддержка: все современные браузеры. Полифилы пригодятся только для очень старых версий.
Быстрый старт: минимальный Custom Element
Создадим простой элемент, который приветствует по имени.
class HelloWorld extends HTMLElement {
connectedCallback() {
const name = this.getAttribute('name') || 'мир';
this.textContent = `Привет, ${name}!`;
}
}
customElements.define('hello-world', HelloWorld);
<hello-world name='Аня'></hello-world>
Важно: имя кастомного элемента обязательно содержит дефис (например, hello-world), иначе браузер отклонит регистрацию.
Shadow DOM и шаблон: изолируем стили
Теперь сделаем полноценный компонент карточки пользователя с закрытой разметкой и стилями.
<template id='user-card-tpl'>
<style>
:host {
display: block;
font: 14px/1.4 system-ui, sans-serif;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
background: #fff;
}
.name { font-weight: 600; }
.meta { color: #6b7280; }
::slotted(img) { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; }
</style>
<div class='row'>
<slot name='avatar'></slot>
<div class='info'>
<div class='name' part='name'><slot name='name'>Без имени</slot></div>
<div class='meta'><slot name='meta'>Пользователь</slot></div>
</div>
</div>
</template>
class UserCard extends HTMLElement {
constructor() {
super();
const tpl = document.getElementById('user-card-tpl');
const root = this.attachShadow({ mode: 'open' });
root.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define('user-card', UserCard);
<user-card>
<img slot='avatar' src='avatar.jpg' alt='Аватар'>
<span slot='name'>Иван Петров</span>
<span slot='meta'>Frontend разработчик</span>
</user-card>
Здесь мы используем слоты: внешний HTML передаёт картинку и тексты внутрь компонента, а стили остаются инкапсулированными в Shadow DOM.
Атрибуты, свойства и жизненный цикл
Добавим реакцию на изменение атрибутов с помощью observedAttributes и attributeChangedCallback.
class BadgeDot extends HTMLElement {
static get observedAttributes() { return ['color', 'size']; }
constructor() {
super();
this._root = this.attachShadow({ mode: 'open' });
this._root.innerHTML = `
<style>
:host{display:inline-block;vertical-align:middle}
.dot{border-radius:50%;display:inline-block}
</style>
<span class='dot' part='dot'></span>`;
this._dot = this._root.querySelector('.dot');
this._render();
}
attributeChangedCallback() { this._render(); }
_render() {
const size = Number(this.getAttribute('size') || 8);
const color = this.getAttribute('color') || '#10b981';
this._dot.style.width = size + 'px';
this._dot.style.height = size + 'px';
this._dot.style.background = color;
}
}
customElements.define('badge-dot', BadgeDot);
Статус: <badge-dot color='#ef4444' size='10'></badge-dot>
Совет: синхронизируйте атрибуты и JS-свойства. Например, сделайте get/set для удобства: element.size = 12;.
Стилизация компонентов снаружи
Глобальные стили внутрь Shadow DOM не проникают. Чтобы разрешить внешнюю стилизацию отдельных частей, используйте механизмы parts/::part и псевдоклассы :host, ::slotted.
/* внутри компонента уже есть part='name' */
/* снаружи страницы: */
user-card::part(name){ color:#2563eb; }
/* состояние компонента через атрибуты и :host */
/* внутри shadow CSS */
:host([variant='warning']){ border-color:#f59e0b; background:#fffbeb; }
Коммуникация: события из компонента
Компонент часто должен сообщать о действиях наружу. Для этого диспатчим кастомные события.
class ToggleSwitch extends HTMLElement {
constructor(){
super();
const root = this.attachShadow({mode:'open'});
root.innerHTML = `
<style>
:host{display:inline-block;cursor:pointer;user-select:none}
.track{width:42px;height:24px;background:#e5e7eb;border-radius:12px;position:relative;transition:.2s}
.thumb{width:20px;height:20px;background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.2)}
:host([checked]) .track{background:#22c55e}
:host([checked]) .thumb{left:20px}
</style>
<div class='track' role='switch' aria-checked='false' tabindex='0'>
<div class='thumb'></div>
</div>`;
this._track = root.querySelector('.track');
this.addEventListener('click', () => this.toggle());
this.addEventListener('keydown', e => { if(e.key===' '||e.key==='Enter'){ e.preventDefault(); this.toggle(); }});
}
toggle(){
const next = !this.hasAttribute('checked');
this.toggleAttribute('checked', next);
this._track.setAttribute('aria-checked', String(next));
this.dispatchEvent(new CustomEvent('change', { detail:{ checked: next }, bubbles:true, composed:true }));
}
}
customElements.define('toggle-switch', ToggleSwitch);
<toggle-switch id='t1'></toggle-switch>
<script>
document.getElementById('t1').addEventListener('change', e => {
console.log('Состояние:', e.detail.checked);
});
</script>
Флаги bubbles:true и composed:true позволяют событию выйти из Shadow DOM и быть пойманным в документе.
Доступность и SEO
- Добавляйте роли и aria-атрибуты внутри Shadow DOM (пример выше: role='switch').
- Убедитесь, что компонент фокусируемый и управляемый с клавиатуры.
- Текстовый контент в слотах индексируется как обычный HTML.
Типичные ошибки новичков
- Отсутствует дефис в названии элемента — регистрация не пройдёт.
- Ожидание, что глобальный CSS применится к Shadow DOM — нет, используйте ::part, :host, ::slotted.
- Избыточные перерисовки в attributeChangedCallback — кэшируйте ссылки на узлы и обновляйте точечно.
- Манипуляции innerHTML с непроверенными данными — риск XSS. Шаблоны и безопасные вставки предпочтительнее.
Производительность: коротко по делу
- Клонируйте готовый <template> вместо конкатенации строк.
- Повторно используйте ссылки на элементы (this._node) и избегайте лишних querySelector.
- Для больших библиотек стилей рассмотрите adoptedStyleSheets (Constructable Stylesheets).
Когда Web Components особенно уместны
- UI-библиотека, независимая от фреймворков.
- Виджеты для вставки на сторонние сайты (инкапсуляция стилей спасает).
- Долгоживущие проекты, где важна стабильность нативного стека.
Хотите уверенно верстать интерфейсы, понимать каскад, шрифты, сетки и собирать аккуратные компоненты? Самое время прокачаться: загляните в практический курс по вёрстке «Вёрстка сайта с нуля 2.0» — он отлично дополняет тему Web Components и ускорит прогресс.
Итоги
Мы разобрали ключевые части Web Components: Custom Elements, Shadow DOM, шаблоны и слоты; подключили стили, атрибуты, события и доступность. Этого достаточно, чтобы начать собирать собственную библиотеку нативных UI-элементов без зависимостей. Дальше можно углубляться в ::part/::theme, adoptedStyleSheets и form-associated элементы. Удачной разработки!
-
Создано 11.05.2026 17:01:25
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.