Разработка Web Components (Custom Elements) для сайта
Интеграция сложной UI-логики в проект без фреймворков — головная боль. Мы часто сталкиваемся с задачей создать переиспользуемый элемент, который одинаково хорошо работает и в WordPress, и в Laravel Blade, и в статическом HTML. Решение — Web Components — набор нативных браузерных API, которые позволяют разрабатывать собственные HTML-элементы с инкапсулированной логикой и стилями. Никаких зависимостей от React, Vue или jQuery. Компоненты живут в любом HTML-контексте.
Три составляющих: Custom Elements API (регистрация нового тега), Shadow DOM (инкапсуляция стилей), HTML Templates (шаблонизация). Используются независимо или вместе. Опыт нашей команды — 5+ лет разработки нативными средствами, более 10 проектов с Web Components. Гарантируем кросс-браузерную совместимость и производительность.
Основные проблемы при интеграции кастомных элементов
Первая проблема — конфликт стилей. Без Shadow DOM встроенные стили могут переопределяться глобальными CSS-правилами. Вторая — отсутствие реактивности: в отличие от фреймворков, нативные элементы не обновляются автоматически при изменении атрибутов. Третья — сложность тестирования: требуется среда с поддержкой Custom Elements. Мы решаем эти проблемы за счёт продуманной архитектуры и использования TypeScript.
Преимущества Web Components
Главное преимущество — независимость от фреймворков. Компонент, написанный один раз, работает в любом проекте: React, Vue, Angular, Svelte, чистый HTML. Это сокращает время разработки на 30% и упрощает поддержку. Кроме того, Web Components не требуют бандлера — их размер нулевой, в отличие от React-компонентов, которым нужно ~40 КБ библиотеки. Для старого проекта на jQuery это идеальный способ добавить современную функциональность без миграции всего стека.
Как создать кастомный элемент с нуля?
Процесс состоит из пяти шагов:
- Спроектировать API компонента — определить атрибуты, события и публичные методы.
- Написать класс, унаследованный от HTMLElement.
- Реализовать методы жизненного цикла (connectedCallback, attributeChangedCallback и т.д.).
- Зарегистрировать элемент через customElements.define().
- Протестировать в нескольких браузерах.
Вот типовой пример — компонент уведомления:
class ToastNotification extends HTMLElement {
private shadow: ShadowRoot
private messageEl: HTMLElement | null = null
static get observedAttributes() {
return ['type', 'message', 'duration']
}
constructor() {
super()
this.shadow = this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.render()
const duration = parseInt(this.getAttribute('duration') || '3000')
if (duration > 0) {
setTimeout(() => this.dismiss(), duration)
}
}
disconnectedCallback() {
this.messageEl?.removeEventListener('click', this.dismiss)
}
attributeChangedCallback(name: string, oldVal: string, newVal: string) {
if (oldVal !== newVal && this.isConnected) {
this.render()
}
}
private render() {
const type = this.getAttribute('type') || 'info'
const message = this.getAttribute('message') || ''
this.shadow.innerHTML = `
<style>
:host {
display: block;
font-family: inherit;
}
.toast {
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
animation: slide-in 0.3s ease;
}
.toast--info { background: #1a1a2e; color: #7eb8f7; border: 1px solid #2a4a7f; }
.toast--success { background: #0d2e1a; color: #5cb85c; border: 1px solid #1a5e30; }
.toast--error { background: #2e0d0d; color: #e74c3c; border: 1px solid #7f1a1a; }
@keyframes slide-in {
from { transform: translateY(-10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
</style>
<div class="toast toast--${type}" part="toast">
${message}
</div>
`
this.messageEl = this.shadow.querySelector('.toast')
this.messageEl?.addEventListener('click', this.dismiss)
}
private dismiss = () => {
this.dispatchEvent(new CustomEvent('toast-dismiss', {
bubbles: true,
composed: true,
}))
this.remove()
}
show(message: string, type = 'info') {
this.setAttribute('message', message)
this.setAttribute('type', type)
if (!this.isConnected) {
document.body.appendChild(this)
}
}
}
customElements.define('toast-notification', ToastNotification)
Использовать можно и через HTML, и через JS.
Жизненный цикл и TypeScript
Custom Elements предоставляют четыре метода жизненного цикла: constructor, connectedCallback, disconnectedCallback, attributeChangedCallback. Для корректной интеграции с TypeScript нужно объявить типы в HTMLElementTagNameMap:
declare global {
interface HTMLElementTagNameMap {
'toast-notification': ToastNotification
}
}
Типизация custom elements — важный шаг для создания бизнес-компонентов с автодополнением в IDE.
| Метод | Когда вызывается | Что делать |
|---|---|---|
| constructor | При создании элемента | Инициализация состояния, создание Shadow DOM |
| connectedCallback | При добавлении в DOM | Установка обработчиков, загрузка данных |
| disconnectedCallback | При удалении из DOM | Очистка обработчиков, таймеров |
| attributeChangedCallback | При изменении отслеживаемого атрибута | Обновление представления |
Когда Shadow DOM не нужен?
Shadow DOM добавляет изоляцию, но и сложность. Для простых элементов (например, счётчик или раскрывающийся список) достаточно Light DOM — стили наследуются от родителя, код проще. Пример аккордеона:
class AccordionItem extends HTMLElement {
private header!: HTMLElement
private content!: HTMLElement
private isOpen = false
connectedCallback() {
this.innerHTML = `
<button class="accordion-header" aria-expanded="false">
<slot name="header"></slot>
<svg class="accordion-icon" viewBox="0 0 24 24">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div class="accordion-content" role="region" hidden>
<slot name="content"></slot>
</div>
`
this.header = this.querySelector('.accordion-header')!
this.content = this.querySelector('.accordion-content')!
this.header.addEventListener('click', this.toggle)
}
private toggle = () => {
this.isOpen = !this.isOpen
this.header.setAttribute('aria-expanded', String(this.isOpen))
this.content.hidden = !this.isOpen
}
disconnectedCallback() {
this.header?.removeEventListener('click', this.toggle)
}
}
customElements.define('accordion-item', AccordionItem)
Сравнение подходов
| Параметр | Shadow DOM | Light DOM |
|---|---|---|
| Инкапсуляция стилей | Полная | Нет |
| Сложность | Выше | Ниже |
| Производительность | Одинаково | Одинаково |
| Подходит для | Библиотек, сложных виджетов | Простых интерактивных элементов |
| Подход | Время на первый компонент | Повторное использование | Зависимости |
|---|---|---|---|
| Web Components | 4-8 часов | 100% в любом проекте | Нет |
| React компонент | 2-4 часа | только в React | 40KB |
| Vue компонент | 2-4 часа | только в Vue | 30KB |
Что входит в работу?
- Проектирование API компонента
- Реализация на TypeScript с полным жизненным циклом
- Написание деклараций типов
- Интеграция с существующим проектом
- Документация по использованию и кастомизации
- Поддержка в течение месяца после сдачи
Сроки и стоимость
Один простой компонент без Shadow DOM — от 4 до 8 часов. Комплексный компонент с анимациями, тестами и TypeScript — 1–3 дня. Библиотека из 5–10 компонентов — 1–2 недели. Стоимость рассчитывается индивидуально под ваш проект. Свяжитесь с нами для бесплатной консультации — мы оценим внедрение и предложим оптимальное решение.
На одном из проектов для крупного банка мы разработали библиотеку из 8 кастомных элементов (аккордеон, табы, уведомления, дропдаун, модальное окно, пагинатор, карточка товара, слайдер) для замены jQuery-плагинов. В результате размер страницы уменьшился на 200 КБ, а скорость загрузки выросла на 15%. При этом компоненты использовались и в WordPress, и в React-админке без изменений. Такая экономия ресурсов — прямое следствие отсутствия зависимостей и лёгкости нативных элементов.
Закажите разработку Web Components под ключ — получите надёжные, независимые элементы для любого сайта. Получите консультацию — мы ответим на все вопросы и поможем выбрать оптимальный подход.







