Компонентно-ориентированный подход уже давно зарекомендовал себя как отличная практика разработки. Его массовая популярность пришла вместе с такими библиотеками, как React и Vue. Создавая компоненты, мы чётко разграничиваем логику, формируем зоны ответственности и эффективно боремся с дублированием кода. Обычно компонент отвечает за рендеринг HTML-разметки и динамически обновляет её в зависимости от своего состояния. Кроме того, ключевую роль играют механизмы контроля жизненного цикла, например, обработка этапов: «компонент присоединился», «компонент обновился» и «компонент был удалён». Это база, но часто существует и множество других хуков.
Раньше для работы с этой парадигмой мы были вынуждены использовать React, Vue или аналогичные фреймворки. Однако сегодня можно обойтись без дополнительных библиотек и обязательной сложной сборки, потому что компоненты доступны «из коробки» в современных браузерах. Да, я говорю о Веб-компонентах. Если быть точнее, о Пользовательских элементах (Custom Elements), поскольку «Веб-компоненты» — это скорее набор стандартных технологий, позволяющих создавать эти самые элементы.
Прежде чем углубляться в детали, давайте проверим, насколько эта технология готова к использованию в продакшене. Для этого зайдём на сайт, который все мы часто открываем для проверки поддержки функций браузерами, — на https://caniuse.com/

Действительно, у Safari до сих пор нет полной поддержки всех спецификаций. Однако важно понимать: Web Components — это обширная тема, и основная функциональность — создание абсолютно новых элементов (автономных пользовательских элементов, или Autonomous Custom Elements) — работает стабильно и без проблем во всех современных браузерах, включая Safari.
Проблема в Safari касается лишь одной конкретной, хотя и важной, возможности: наследования и расширения логики встроенных элементов (таких как <button>, <input>), которые называются «пользовательскими встроенными элементами» (Customized Built-in Elements). Эта часть спецификации до сих пор не реализована в WebKit.
Ну и как сказал один многоуважаемый человек:

Давайте приступим к практической части, создадим веб элемент — для этого нам потребуется html файл с базовой разметкой:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> </head> <body> <custom-counter></custom-counter> </body> </html>
Создадим элемент custom-counter это будет кнопка с счетчиком при клике на кнопку значения счетчика будет увеличиваться.
На данный момент компонент не определен и он абсолютно пустой стили также отсутствуют:

Браузер не блокирует рендеринг контента, помещённого внутрь элемента, до того как загрузится и инициализируется определение самого веб-компонента. Такой подход обеспечивает прогрессивное улучшение и является несомненным плюсом для SEO-оптимизации, поскольку поисковые роботы видят и индексируют исходный HTML-контент без задержек.

Давайте наполним тело компонента и напишем стили. Стили и скрпты следует писать в отдельных файлах, тут так сделано в качестве демонстрации:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> <style> body { display: flex; justify-content: center; } custom-counter { display: flex; flex-direction: column; align-items: center; } </style> </head> <body> <custom-counter> <h1 data-id="count">0</h1> <button name="add">Add</button> </custom-counter> </body> </html>

Осталось добавить логику, при клике на кнопку увеличивать значение на 1. Добавим скрипт: Создадим класс который должен наслдеовать HTMLElement. А также нужно сообщить бразуеру (customElements.define) что для кастомного элемента custom-counter существует такой класс: CustomeCounter и нужно его обрабатывать. Если все сделали верно то на странице в консоле должно быть сообщение: "Подключился!"
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> <style> body { display: flex; justify-content: center; } custom-counter { display: flex; flex-direction: column; align-items: center; } </style> </head> <body> <custom-counter> <h1 data-id="count">0</h1> <button name="add">Add</button> </custom-counter> <script> class CustomeCounter extends HTMLElement { constructor () { super() } connectedCallback () { console.log('Подключился!') this.addEventListener('click', event => { const { target } = event }) } } customElements.define('custom-counter', CustomeCounter) </script> </body> </html>
Теперь реализуем обработку кликов по кнопке. Чтобы постоянно не обращаться к DOM в поисках элемента счётчика, я вынес его определение в конструктор класса, сохранив в свойстве this.countEl
class CustomeCounter extends HTMLElement { constructor () { super() this.countEl = this.querySelector('[data-id=count]') } connectedCallback () { console.log('Подключился!') this.addEventListener('click', event => { const { target } = event const addBtnEl = target.closest('button[name=add]') if (!addBtnEl) return this.countEl.innerText = Number(this.countEl.innerText) + 1 }) } }
Таким образом при клике на кнопку значение счетчика будет увеличиваться:

Всё работает! Однако это лишь один из способов создания Веб-компонентов. Существуют и другие: элементы с Shadow DOM для полной инкапсуляции, Declarative Shadow DOM для серверного рендеринга, а также использование слотов (slot) и шаблонов (template) для создания гибкой структуры. Я планирую подробно рассказать об этих подходах в следующей статье.
А сейчас я покажу несколько способов, как создавать элемент, не дублируя постоянно его разметку в HTML-коде.
Способ 1 (выносим тело компонента в script):
<custom-counter></custom-counter> <script> class CustomeCounter extends HTMLElement { constructor () { super() this.innerHTML = `<h1 data-id="count">0</h1> <button name="add">Add</button>` this.countEl = this.querySelector('[data-id=count]') } connectedCallback () { console.log('Подключился!') this.addEventListener('click', event => { const { target } = event const addBtnEl = target.closest('button[name=add]') if (!addBtnEl) return this.countEl.innerText = Number(this.countEl.innerText) + 1 }) } } customElements.define('custom-counter', CustomeCounter) </script>
Как видите, мы добавили HTML-разметку компонента прямо в конструкторе. У этого подхода есть существенный недостаток: при первоначальной загрузке страницы тело компонента будет отсутствовать в DOM. Пользователю придётся дождаться не только загрузки скрипта (если он подключён через src), но и его выполнения, прежде чем компонент примет свой окончательный вид.
Способ 2 (создаем template в HTML):
<template id="custom-counter-template"> <h1 data-id="count">0</h1> <button name="add">Add</button> </template> <custom-counter></custom-counter> <script> class CustomeCounter extends HTMLElement { constructor () { super() const templateContent = document.getElementById('custom-counter-template').content.cloneNode(true) this.appendChild(templateContent) this.countEl = this.querySelector('[data-id=count]') } connectedCallback () { console.log('Подключился!') this.addEventListener('click', event => { const { target } = event const addBtnEl = target.closest('button[name=add]') if (!addBtnEl) return this.countEl.innerText = Number(this.countEl.innerText) + 1 }) } } customElements.define('custom-counter', CustomeCounter) </script>
Этот подход отличается от предыдущего тем, что вместо описания HTML внутри скрипта, мы вынесли разметку на уровень HTML-документа, в тело страницы. Это обеспечивает большую гибкость: одна и та же логика компонента может работать с разными шаблонами на разных страницах.
Подведём итоги. В этой статье я показал, как создать пользовательский элемент — от регистрации в браузере до добавления интерактивности. Мы рассмотрели несколько практических подходов к формированию его структуры.
Однако это лишь начало знакомства с миром Веб-компонентов. Впереди — такие важные темы, как изоляция стилей через Shadow DOM, композиция с помощью слотов <slot>, тонкости жизненного цикла и многое другое. Всему этому будут посвящены следующие материалы.
Спасибо за внимание!
