Logo Craft Homelab Docs Контакты Telegram
Web Components: стандарты браузера — Custom Elements
Sat Dec 20 2025

Web Components: нативные компоненты браузера

Web Components — стандарт для создания переиспользуемых компонентов с инкапсуляцией. Это набор нативных API браузера, позволяющих создавать компоненты без фреймворков. Компоненты работают в любом современном браузере и совместимы с React, Vue, Angular.

Custom Elements

Custom Elements позволяют создавать собственные HTML-теги с инкапсулированной логикой и стилями.

class MyElement extends HTMLElement

Базовый класс для всех custom elements — HTMLElement. Расширяя его, вы создаёте новый тег.

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 16px;
          border: 1px solid #ddd;
          border-radius: 8px;
        }
        .title {
          font-weight: bold;
          font-size: 1.2em;
        }
      </style>
      <div class="title"><slot name="title">Default Title</slot></div>
      <div class="content"><slot></slot></div>
    `;
  }
}

customElements.define('my-card', MyCard);

Метод attachShadow создаёт Shadow DOM — изолированное дерево элементов. Стили внутри Shadow DOM не влияют на остальную страницу.

Lifecycle callbacks

Web Components имеют хуки жизненного цикла для управления поведением:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // Создание shadow root
    // Вызывается всегда при создании элемента
  }

  connectedCallback() {
    // Добавлен в DOM
    // Инициализация, загрузка данных
  }

  disconnectedCallback() {
    // Удалён из DOM
    // Очистка, отписка от событий
  }

  attributeChangedCallback(name, oldVal, newVal) {
    // Изменение атрибута
    // Только для атрибутов из observedAttributes
  }

  static get observedAttributes() {
    return ['title', 'variant'];
  }
}

Эти хуки аналогичны useEffect в React или onMounted в Vue.

Observed attributes

observedAttributes позволяет компоненту реагировать на изменения HTML-атрибутов:

class MyButton extends HTMLElement {
  static get observedAttributes() {
    return ['disabled', 'variant'];
  }
  
  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'disabled') {
      this.render();
    }
  }
}

Shadow DOM

Mode: open vs closed

// open - можно получить через element.shadowRoot
const shadow = element.attachShadow({ mode: 'open' });

// closed - shadowRoot = null (не рекомендуется)
const shadow = element.attachShadow({ mode: 'closed' });

Styles

class MyElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
        }
        :host([hidden]) {
          display: none;
        }
        :host(.primary) {
          --btn-color: blue;
        }
        ::slotted(*) {
          color: gray;
        }
      </style>
      <button class="btn"><slot></slot></button>
    `;
  }
}

HTML Templates

<template id="my-template">
  <style>
    .card {
      padding: 16px;
      border: 1px solid #ddd;
    }
  </style>
  <div class="card">
    <slot name="header"></slot>
    <slot></slot>
  </div>
</template>

<script>
  class MyCard extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      const template = document.getElementById('my-template');
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
  customElements.define('my-card', MyCard);
</script>

Slots

<my-card>
  <span slot="title">Заголовок</span>
  <p>Контент</p>
</my-card>
// Доступ к slot
const slot = this.shadowRoot.querySelector('slot');
const assigned = slot.assignedNodes();

Events

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.shadowRoot.querySelector('button')
      .addEventListener('click', this._handleClick.bind(this));
  }
  
  _handleClick(e) {
    this.dispatchEvent(new CustomEvent('my-click', {
      bubbles: true,
      composed: true,
      detail: { message: 'clicked' }
    }));
  }
}
// Listening
document.querySelector('my-button')
  .addEventListener('my-click', (e) => console.log(e.detail));

Properties

class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['count'];
  }
  
  get count() {
    return this.getAttribute('count');
  }
  
  set count(value) {
    this.setAttribute('count', value);
  }
  
  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'count') {
      this.render();
    }
  }
}

Пример: Modal

class MyModal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  
  connectedCallback() {
    this.render();
    this._setupEvents();
  }
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: none;
        }
        :host([open]) {
          display: flex;
          position: fixed;
          inset: 0;
          background: rgba(0,0,0,0.5);
          align-items: center;
          justify-content: center;
        }
        .modal {
          background: white;
          padding: 24px;
          border-radius: 8px;
          min-width: 300px;
        }
        .close {
          float: right;
          cursor: pointer;
        }
      </style>
      <div class="modal">
        <span class="close">&times;</span>
        <slot name="title"></slot>
        <slot></slot>
      </div>
    `;
  }
  
  _setupEvents() {
    this.shadowRoot.querySelector('.close')
      .addEventListener('click', () => {
        this.removeAttribute('open');
      });
  }
  
  static get observedAttributes() {
    return ['open'];
  }
}

customElements.define('my-modal', MyModal);

Использование

<my-modal open>
  <span slot="title">Заголовок модального окна</span>
  <p>Контент модального окна</p>
</my-modal>

<my-button variant="primary">Нажми меня</my-button>

Заключение

Web Components предоставляют стандартный способ создания переиспользуемых компонентов с полной инкапсуляцией.