Розробка Web Components (Custom Elements) для сайту
Інтеграція складної UI-логіки в проєкт без фреймворків — головний біль. Ми часто стикаємося із задачею створити перевикористовуваний елемент, який однаково добре працює і в WordPress, і в Laravel Blade, і в статичному HTML. Рішення — Web Components — набір нативних браузерних API, які дозволяють розробляти власні HTML-елементи з інкапсульованою логікою та стилями. Жодних залежностей від React, Vue або jQuery. Компоненти живуть в будь-якому HTML-контексті.
Три складові: Custom Elements API (реєстрація нового тега), Shadow DOM (інкапсуляція стилів), HTML Templates (шаблонізація). Використовуються незалежно або разом. Досвід нашої команди — 10+ років у продакшені (Бітрікс, 1С, веб-розробка), 5+ років розробки нативними засобами, понад 10 проєктів з Web Components. Гарантуємо крос-браузерну сумісність та продуктивність.
Основні проблеми при інтеграції кастомних елементів
Перша проблема — конфлікт стилів. Без Shadow DOM вбудовані стилі можуть перевизначатися глобальними CSS-правилами. Друга — відсутність реактивності: на відміну від фреймворків, нативні елементи не оновлюються автоматично при зміні атрибутів. Третя — складність тестування: потрібне середовище з підтримкою Custom Elements. Ми вирішуємо ці проблеми за рахунок продуманої архітектури та використання TypeScript.
Як ми це робимо (доказ експертності)
На одному з проєктів для великого банку ми розробили бібліотеку з 8 кастомних елементів (акордеон, таби, сповіщення, дропдаун, модальне вікно, пагінатор, картка товару, слайдер) для заміни jQuery-плагінів. Результат: розмір сторінки зменшився на 200 КБ, а швидкість завантаження зросла на 15%. При цьому компоненти використовувалися і в WordPress, і в React-адмінці без змін. Така економія ресурсів — прямий наслідок відсутності залежностей та легкості нативних елементів.
Як створити кастомний елемент з нуля?
Процес складається з п'яти кроків:
- Спроектувати 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 тижні. Вартість розраховується індивідуально після аналізу вашого проєкту. Зв'яжіться з нами для безкоштовної консультації — ми оцінимо впровадження та запропонуємо оптимальне рішення.
Типові помилки при розробці Custom Elements
Одна з найпоширеніших помилок — забути зареєструвати елемент до його використання в HTML. Інша — використовувати Shadow DOM там, де достатньо Light DOM, що ускладнює стилізацію ззовні. Також часто ігнорують метод disconnectedCallback, що призводить до витоку пам'яті через неповне зняття обробників. Ми завжди перевіряємо ці моменти.
Замовте розробку Web Components під ключ — отримайте надійні, незалежні елементи для будь-якого сайту. Отримайте консультацію — ми відповімо на всі питання та допоможемо обрати оптимальний підхід.







