
Продолжая тему из моей предыдущей статьи о веб-компонентах, я хочу подробнее рассмотреть их применение для решения реальных задач. Сегодня мы напишем простую, но полнофункциональную реализацию Слайдера, в процессе познакомившись с такими ключевыми концепциями, как Shadow DOM и Declarative Shadow DOM.
Что нам даёт использование Shadow DOM:
Возможность работать со слотами (
<slot>) для композиции контентаПолная изоляция стилей компонента от глобальных таблиц CSS
Инкапсуляция DOM-дерева компонента
Итак, существует два основных способа создания веб-компонента с Shadow DOM:
Императивный подход (в JavaScript-коде): использование метода
this.attachShadow({ mode: "open" })внутри класса компонента.Декларативный подход (в HTML-разметке): с помощью атрибута
shadowrootmode="open", который добавляется непосредственно к элементу<template>.
Рассмотрим каждый подробнее
Императивный подход
Рассмотрим базовую реализацию. Вот код, который создаёт основу для нашего компонента:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components - Slider</title> <link rel="stylesheet" href="/app.css" /> </head> <body> <slider-component></slider-component> </body> <script> class SliderComponent extends HTMLElement { constructor() { super() this._shadowRoot = this.attachShadow({ mode: "open" }) } } customElements.define('slider-component', SliderComponent) </script> </html
Как видите, в конструкторе класса мы вызываем метод attachShadow с параметром mode: "open". Это создаёт открытое (open) Shadow DOM-дерево, прикреплённое к нашему элементу.
В инструментах разработки Chrome (DevTools) теперь можно увидеть, что элемент <slider-component> содержит #shadow-root (open), который визуально подсвечивается, показывая границы изолированного DOM-поддерева.

Декларативный подход
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components - Slider</title> <link rel="stylesheet" href="/app.css" /> </head> <body> <slider-component> <template shadowrootmode="open"></template> </slider-component> </body> <script> class SliderComponent extends HTMLElement { constructor() { super() } } customElements.define('slider-component', SliderComponent) </script> </html>
Мы добились аналогичного результата — компонент теперь использует Shadow DOM. Однако ключевое отличие в том, что при декларативном подходе Shadow DOM формируется немедленно, в момент загрузки HTML, ещё до выполнения какого-либо JavaScript-кода. Это открывает возможности для SSR (Server-Side Rendering) и обеспечивает более предсказуемое отображение контента.
Теперь продолжим создание слайдера. Для этого наполним наш компонент и приложение стилями. Создадим отдельный файл CSS для приложения а также один д��я изолированных стилей.
app.css
body { display: flex; padding: 1em; --bg: white; --shadow: 1px solid #eee; --slider--gap: 1em; } .slider { width: 305px; } .slider__item { background-color: #eee; width: 80px; height: 80px; border-radius: .8em; }
slider.css:
:host { position: relative; display: flex; align-items: center; width: 100%; padding: 0 1em; box-sizing: border-box; } .slides { display: flex; gap: var(--slider--gap, 1em); overflow-x: scroll; scroll-snap-type: x mandatory; scrollbar-width: none; -ms-overflow-style: none; } ::slotted(.slider__item) { flex-shrink: 0; scroll-snap-align: center; } :host::-webkit-scrollbar { display: none; } .navigation-control { position: absolute; width: 2em; height: 2em; background-color: var(--bg); border-radius: 50%; box-shadow: var(--shadow); display: flex; align-items: center; display: flex; align-items: center; justify-content: center; opacity: .5; outline: none; transition: .3s; &:hover, &:focus { opacity: 1; transform: scale(1.1); } } .navigation-control__left { left: .3em; } .navigation-control__right { right: .3em; }
Как уже упоминалось ранее, ключевое преимущество Shadow DOM — это полная изоляция стилей. Классы, объявленные внутри CSS-файла, подключённого к компоненту, не будут конфликтовать с глобальными стилями и не попадут под влияние внешних CSS-правил.
Кроме того, появляется доступ к специальным CSS-селекторам, таким как:
:host— для стилизации самого элемента-хозяина;::slotted()— для стилизации контента, проецируемого в слоты.
Подробную информацию обо всех доступных селекторах и областях их применения можно найти в документации на MDN.
И ещё один важный нюанс: несмотря на изоляцию, CSS-переменные (Custom Properties) наследуются через границы Shadow DOM. Это позволяет гибко настраивать тему компонента извне. Отличной практикой является указание значения по умолчанию на случай, если переменная не задана: var(--slider--gap, 1em)
Теперь разберём структуру HTML для нашего slider-component. Всё содержимое, расположенное внутри элемента <template>, будет помещено в Shadow DOM компонента. Контент, размещённый непосредственно между тегами <slider-component></slider-component> в основном документе, будет проецироваться внутрь элемента <slot>, определённого в шаблоне.
index.html:
<slider-component> <template shadowrootmode="open"> <link rel="stylesheet" href="/slider.css"> <div class="navigation-control navigation-control__left" tabindex="0" data-control="prev"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" height="1em" width="1em"> <path d="M642-48 209-480l433-432 103 103-329 329 329 329L642-48Z" /> </svg> </div> <div class="navigation-control navigation-control__right" tabindex="0" data-control="next"> <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em"> <path d="M321-48 218-151l329-329-329-329 103-103 432 432L321-48Z" /> </svg> </div> <slot class="slides" data-id="slides"></slot> </template> <h1>Hello World!</h1> </slider-component>
В инструментах разработки Chrome (DevTools):

Слоты можно именовать. Для этого используется атрибут name: <slot name="slot-1"></slot>. Чтобы направить контент именно в этот слот, элементу в основном документе нужно указать соответствующий атрибут slot: <h1 slot="slot-1">Hello World!</h1>:
<slider-component> <template shadowrootmode="open"> <link rel="stylesheet" href="/slider.css"> <div class="navigation-control navigation-control__left" tabindex="0" data-control="prev"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" height="1em" width="1em"> <path d="M642-48 209-480l433-432 103 103-329 329 329 329L642-48Z" /> </svg> </div> <div class="navigation-control navigation-control__right" tabindex="0" data-control="next"> <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em"> <path d="M321-48 218-151l329-329-329-329 103-103 432 432L321-48Z" /> </svg> </div> <slot class="slides" data-id="slides"></slot> </template> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> <div class="slider__item" data-id="slider-item"></div> </slider-component>
Добавим корневому элементу slider-component CSS-класс slider:
<slider-component class="slider"></slider-component>
Поскольку этот класс применяется к самому пользовательскому элементу (к «хосту»), а не к содержимому внутри Shadow DOM, он будет доступен в глобальной области видимости. Соответственно, стили для этого класса будут применяться из основной таблицы стилей app.css.
Осталось добавить логику обработки клика:
class SliderComponent extends HTMLElement { constructor() { super() this.slidesEl = this.shadowRoot.querySelector('[data-id=slides]') } get deltaX() { return this.hasAttribute('delta-x') ? Number(this.getAttribute('delta-x')) : 100 } connectedCallback() { this.shadowRoot.addEventListener('click', e => { const { target } = e const controlEl = target.closest('[data-control]') if (!controlEl) { return } const { control } = controlEl.dataset if (control === 'next') { this.#handleNext() } else if (control === 'prev') { this.#handlePrev() } else { console.error('Invalid control value') } }) } #handleNext() { this.slidesEl.scrollTo({ behavior: "smooth", left: this.slidesEl.scrollLeft + this.deltaX }) } #handlePrev() { this.slidesEl.scrollTo({ behavior: "smooth", left: this.slidesEl.scrollLeft - this.deltaX }) } } customElements.define('slider-component', SliderComponent)
Итак, в конструкторе я добавил ссылку на элемент слайдера this.slidesEl, чтобы избежать многократного поиска в DOM при каждом клике.
Также было добавлено вычисляемое свойство deltaX, которое определяет величину сдвига слайдов при клике на кнопки навигации. Его значение берётся из атрибута компонента delta-x или, если атрибут не задан, используется значение по умолчанию — 100.
В методе connectedCallback (который вызывается когда компонент добавляется в DOM) регистрируется обработчик кликов. Он анализирует атрибут data-control нажатой кнопки и определяет направление прокрутки — влево или вправо, после чего выполняет соответствующий сдвиг на вычисленное значение deltaX.
Таким образом, мы получаем готовый к использованию слайдер. Для управления его поведением можно:
Задать атрибут
delta-x=для контроля величины сдвигаНастраивать внешний вид через CSS-переменные, например
--bg--slider--gap
В этой статье мы создали полнофункциональный слайдер с использованием Web Components. Получился универсальный компонент, который можно легко встроить в любой проект и гибко настроить через CSS-переменные и HTML-атрибуты.
В рамках следующей статьи я планирую рассмотреть интеграцию Веб-компонентов с популярными JavaScript-фреймворками и библиотеками, такими как React, Vue и Angular. Мы разберём ключевые аспекты взаимодействия, потенциальные проблемы и лучшие практики совместного использования этих технологий.
