Розробка Web Components (Custom Elements) для сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка Web Components (Custom Elements) для сайту
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка Web Components (Custom Elements) для сайту

Web Components—набір нативних браузерних API, які дозволяють створювати переиспользуемі HTML-елементи з інкапсульованою логікою та стилями. Без фреймворків. Працюють у будь-якому HTML-контексті: WordPress, Laravel Blade, Twig, Hugo, vanilla HTML.

Три компоненти: Custom Elements API (реєстрація нового тегу), Shadow DOM (інкапсуляція стилів), HTML Templates (шаблонізація). Використовуються незалежно або разом.

Custom Elements: основи

class ToastNotification extends HTMLElement {
  private shadow: ShadowRoot
  private messageEl: HTMLElement | null = null

  // Список атрибутів, зміни яких відслідковуються
  static get observedAttributes() {
    return ['type', 'message', 'duration']
  }

  constructor() {
    super()
    // attachShadow створює Shadow DOM
    this.shadow = this.attachShadow({ mode: 'open' })
  }

  // Викликається при додаванні елемента до DOM
  connectedCallback() {
    this.render()

    const duration = parseInt(this.getAttribute('duration') || '3000')
    if (duration > 0) {
      setTimeout(() => this.dismiss(), duration)
    }
  }

  // Викликається при видаленні з DOM
  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,  // пробивається через Shadow DOM boundary
    }))
    this.remove()
  }

  // Публічний метод — викликається з зовнішнього JS
  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 -->
<toast-notification type="success" message="Збережено" duration="4000"></toast-notification>

<script>
  // Через JS
  const toast = document.createElement('toast-notification')
  toast.setAttribute('type', 'error')
  toast.setAttribute('message', 'Щось пішло не так')
  document.body.appendChild(toast)

  // Або через публічний метод (якщо елемент вже зареєстрований)
  const existing = document.querySelector('toast-notification')
  existing.show('Дані завантажені', 'success')
</script>

TypeScript типізація

TypeScript не знає про кастомні елементи—потрібні декларації:

// types/custom-elements.d.ts
declare global {
  interface HTMLElementTagNameMap {
    'toast-notification': ToastNotification
    'dropdown-menu': DropdownMenu
    'modal-dialog': ModalDialog
  }

  namespace JSX {
    interface IntrinsicElements {
      'toast-notification': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement> & {
          type?: 'info' | 'success' | 'error'
          message?: string
          duration?: string
        },
        HTMLElement
      >
    }
  }
}

export {}

Повний lifecycle

class AdvancedElement extends HTMLElement {
  static get observedAttributes() { return ['src', 'lazy']; }

  // Жизненний цикл:
  constructor() {
    super()
    // Тільки ініціалізація Shadow DOM та обробників
    // Не звертаємось до атрибутів — їх ще немає
  }

  connectedCallback() {
    // Елемент додано до DOM
    // Безпечно читати атрибути, звертатися до children
    this.initialize()
  }

  disconnectedCallback() {
    // Очистка: відписки, cancelAnimationFrame, WeakRef cleanup
    this.cleanup()
  }

  adoptedCallback() {
    // Елемент перемищено в інший document (рідко)
    this.reinitialize()
  }

  attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
    if (!this.isConnected) return  // ігноруємо до connectedCallback
    this.onAttributeChange(name, oldVal, newVal)
  }
}

Приклад: Accordion компонент

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))

    if (this.isOpen) {
      this.content.hidden = false
      this.content.style.maxHeight = '0'
      requestAnimationFrame(() => {
        this.content.style.maxHeight = this.content.scrollHeight + 'px'
      })
    } else {
      this.content.style.maxHeight = '0'
      this.content.addEventListener('transitionend', () => {
        if (!this.isOpen) this.content.hidden = true
      }, { once: true })
    }
  }

  disconnectedCallback() {
    this.header?.removeEventListener('click', this.toggle)
  }
}

customElements.define('accordion-item', AccordionItem)
<accordion-item>
  <span slot="header">Як працює доставка?</span>
  <div slot="content">
    <p>Доставляємо по всій країні в течение 3–5 робочих днів.</p>
  </div>
</accordion-item>

Коли не потрібен Shadow DOM

Shadow DOM додає складність. Для простих компонентів без конфліктів стилів достатньо Light DOM:

class SimpleCounter extends HTMLElement {
  private count = 0

  connectedCallback() {
    this.count = parseInt(this.getAttribute('initial') || '0')
    this.render()
  }

  private render() {
    this.innerHTML = `
      <button class="counter-btn counter-btn--dec">-</button>
      <span class="counter-value">${this.count}</span>
      <button class="counter-btn counter-btn--inc">+</button>
    `

    this.querySelector('.counter-btn--inc')!.addEventListener('click', () => {
      this.count++
      this.querySelector('.counter-value')!.textContent = String(this.count)
      this.dispatchEvent(new CustomEvent('change', { detail: this.count, bubbles: true }))
    })

    this.querySelector('.counter-btn--dec')!.addEventListener('click', () => {
      this.count--
      this.querySelector('.counter-value')!.textContent = String(this.count)
      this.dispatchEvent(new CustomEvent('change', { detail: this.count, bubbles: true }))
    })
  }
}

Терміни

Один кастомний елемент без Shadow DOM — 4–8 годин. Бібліотека з 5–10 компонентів з TypeScript, тестами та документацією — 1–2 тижні.