Разработка 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 недели.