Компонентно-ориентированный подход уже давно зарекомендовал себя как отличная практика разработки. Его массовая популярность пришла вместе с такими библиотеками, как 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>, тонкости жизненного цикла и многое другое. Всему этому будут посвящены следующие материалы.
Спасибо за внимание!
Комментарии (9)

isumix
04.11.2025 09:33Вэб компоненты слишком громоздки/многословны:
Покажите мне код
Например вот как может выглядеть компонент кастомного счетчика на Фьюзоре:
const ClickCounter = ({count = 0}) => ( <button click_e_update={() => count++}>Clicked {() => count} times</button> );
Xbolt
04.11.2025 09:33Это разные вещи, и там Вам ответили
Лучше один раз изучить стандарт (W3C), чем для каждого фреймворка запоминать отличия.

zababurin
04.11.2025 09:33Про декларатиынй shadow dom ещё бы написали. Очень важная и определяющая возможность, для самой больной темы серверного рендеринга. С декларативным shadow dom это стало значительно проще.


McLotos
04.11.2025 09:33Очередной некропост о технологии пятнадцатилетней давности. Я это использовал еще до появления всех этих ваших новомодных vue, svelte и т.д.

zababurin
04.11.2025 09:33Я это использовал еще до появления всех этих ваших новомодных vue, svelte и т.д.
Очередной некропост о технологии пятнадцатилетней давности
Раньше 2017 года не могли. Тогда уже vue и react были.
Я как только он появился начал на них писать и это один из агрументов был, что никто не сможет написать, что я это 15 лет уже использую. ))) Раньше меня вы могли начать писать, только если вы являетесь разработчиком этих стандартов.
Сочетание этих технологий стало стандартом к 2018 году.
Да и тогда это фреймворк полимер был, поддержка очень плохая была.
Стандарту 8 лет максимум

Dima-Andreev Автор
04.11.2025 09:33Я наверное немного отстал от жизни) Просто во фронтенде такое многообразие библиотек и фреймворков что все изучить просто нереально (Я до этого писал на React и Vue). Сейчас решил посмотреть на то что есть из коробки и мне это очень понравилось, вот и решил поделится)
Xbolt
Хорошо, но было б замечательно если бы было что-то более детальнее чем на https://learn.javascript.ru/ ,
Dima-Andreev Автор
learn.javascript отличный сайт, там много хорошего материала.
Я бы хотел написать еще пару статей на эту тему, подскажите что можно было бы улучшить? и что вы имеете ввиду под более детальнее? Больше примеров?
Xbolt
Что-нибудь специфическое в использовании, редкое использование. Хитрости в применении.