Web Components (Custom Elements и Shadow DOM): практическое руководство для начинающих

Web Components (Custom Elements и Shadow DOM): практическое руководство для начинающих

Web Components (Custom Elements и Shadow DOM): практическое руководство для начинающих

Если вы хотите делать аккуратные и переиспользуемые интерфейсные блоки без React/Vue, Web Components — то, что нужно. В этом руководстве мы шаг за шагом создадим несколько компонентов, разберём Custom Elements, Shadow DOM, шаблоны, слоты, работу с атрибутами и событиями. Всё на чистом HTML5+JS.

Что такое Web Components

  • Custom Elements — собственные HTML-теги, например <user-card>.
  • Shadow DOM — инкапсуляция разметки и стилей внутри компонента.
  • HTML Templates & Slots — шаблоны для разметки и точки вставки пользовательского контента.
  • Поддержка: все современные браузеры. Полифилы пригодятся только для очень старых версий.

    Быстрый старт: минимальный Custom Element

    Создадим простой элемент, который приветствует по имени.

    class HelloWorld extends HTMLElement {
      connectedCallback() {
        const name = this.getAttribute('name') || 'мир';
        this.textContent = `Привет, ${name}!`;
      }
    }
    customElements.define('hello-world', HelloWorld);
    
    <hello-world name='Аня'></hello-world>
    

    Важно: имя кастомного элемента обязательно содержит дефис (например, hello-world), иначе браузер отклонит регистрацию.

    Shadow DOM и шаблон: изолируем стили

    Теперь сделаем полноценный компонент карточки пользователя с закрытой разметкой и стилями.

    <template id='user-card-tpl'>
      <style>
        :host {
          display: block;
          font: 14px/1.4 system-ui, sans-serif;
          border: 1px solid #e5e7eb;
          border-radius: 8px;
          padding: 12px;
          background: #fff;
        }
        .name { font-weight: 600; }
        .meta { color: #6b7280; }
        ::slotted(img) { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; }
      </style>
      <div class='row'>
        <slot name='avatar'></slot>
        <div class='info'>
          <div class='name' part='name'><slot name='name'>Без имени</slot></div>
          <div class='meta'><slot name='meta'>Пользователь</slot></div>
        </div>
      </div>
    </template>
    
    class UserCard extends HTMLElement {
      constructor() {
        super();
        const tpl = document.getElementById('user-card-tpl');
        const root = this.attachShadow({ mode: 'open' });
        root.appendChild(tpl.content.cloneNode(true));
      }
    }
    customElements.define('user-card', UserCard);
    
    <user-card>
      <img slot='avatar' src='avatar.jpg' alt='Аватар'>
      <span slot='name'>Иван Петров</span>
      <span slot='meta'>Frontend разработчик</span>
    </user-card>
    

    Здесь мы используем слоты: внешний HTML передаёт картинку и тексты внутрь компонента, а стили остаются инкапсулированными в Shadow DOM.

    Атрибуты, свойства и жизненный цикл

    Добавим реакцию на изменение атрибутов с помощью observedAttributes и attributeChangedCallback.

    class BadgeDot extends HTMLElement {
      static get observedAttributes() { return ['color', 'size']; }
    
      constructor() {
        super();
        this._root = this.attachShadow({ mode: 'open' });
        this._root.innerHTML = `
          <style>
            :host{display:inline-block;vertical-align:middle}
            .dot{border-radius:50%;display:inline-block}
          </style>
          <span class='dot' part='dot'></span>`;
        this._dot = this._root.querySelector('.dot');
        this._render();
      }
    
      attributeChangedCallback() { this._render(); }
    
      _render() {
        const size = Number(this.getAttribute('size') || 8);
        const color = this.getAttribute('color') || '#10b981';
        this._dot.style.width = size + 'px';
        this._dot.style.height = size + 'px';
        this._dot.style.background = color;
      }
    }
    customElements.define('badge-dot', BadgeDot);
    
    Статус: <badge-dot color='#ef4444' size='10'></badge-dot>
    

    Совет: синхронизируйте атрибуты и JS-свойства. Например, сделайте get/set для удобства: element.size = 12;.

    Стилизация компонентов снаружи

    Глобальные стили внутрь Shadow DOM не проникают. Чтобы разрешить внешнюю стилизацию отдельных частей, используйте механизмы parts/::part и псевдоклассы :host, ::slotted.

    /* внутри компонента уже есть part='name' */
    /* снаружи страницы: */
    user-card::part(name){ color:#2563eb; }
    
    /* состояние компонента через атрибуты и :host */
    /* внутри shadow CSS */
    :host([variant='warning']){ border-color:#f59e0b; background:#fffbeb; }
    

    Коммуникация: события из компонента

    Компонент часто должен сообщать о действиях наружу. Для этого диспатчим кастомные события.

    class ToggleSwitch extends HTMLElement {
      constructor(){
        super();
        const root = this.attachShadow({mode:'open'});
        root.innerHTML = `
          <style>
            :host{display:inline-block;cursor:pointer;user-select:none}
            .track{width:42px;height:24px;background:#e5e7eb;border-radius:12px;position:relative;transition:.2s}
            .thumb{width:20px;height:20px;background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.2)}
            :host([checked]) .track{background:#22c55e}
            :host([checked]) .thumb{left:20px}
          </style>
          <div class='track' role='switch' aria-checked='false' tabindex='0'>
            <div class='thumb'></div>
          </div>`;
        this._track = root.querySelector('.track');
        this.addEventListener('click', () => this.toggle());
        this.addEventListener('keydown', e => { if(e.key===' '||e.key==='Enter'){ e.preventDefault(); this.toggle(); }});
      }
      toggle(){
        const next = !this.hasAttribute('checked');
        this.toggleAttribute('checked', next);
        this._track.setAttribute('aria-checked', String(next));
        this.dispatchEvent(new CustomEvent('change', { detail:{ checked: next }, bubbles:true, composed:true }));
      }
    }
    customElements.define('toggle-switch', ToggleSwitch);
    
    <toggle-switch id='t1'></toggle-switch>
    <script>
      document.getElementById('t1').addEventListener('change', e => {
        console.log('Состояние:', e.detail.checked);
      });
    </script>
    

    Флаги bubbles:true и composed:true позволяют событию выйти из Shadow DOM и быть пойманным в документе.

    Доступность и SEO

  • Добавляйте роли и aria-атрибуты внутри Shadow DOM (пример выше: role=’switch’).
  • Убедитесь, что компонент фокусируемый и управляемый с клавиатуры.
  • Текстовый контент в слотах индексируется как обычный HTML.
  • Типичные ошибки новичков

  • Отсутствует дефис в названии элемента — регистрация не пройдёт.
  • Ожидание, что глобальный CSS применится к Shadow DOM — нет, используйте ::part, :host, ::slotted.
  • Избыточные перерисовки в attributeChangedCallback — кэшируйте ссылки на узлы и обновляйте точечно.
  • Манипуляции innerHTML с непроверенными данными — риск XSS. Шаблоны и безопасные вставки предпочтительнее.
  • Производительность: коротко по делу

  • Клонируйте готовый <template> вместо конкатенации строк.
  • Повторно используйте ссылки на элементы (this._node) и избегайте лишних querySelector.
  • Для больших библиотек стилей рассмотрите adoptedStyleSheets (Constructable Stylesheets).
  • Когда Web Components особенно уместны

  • UI-библиотека, независимая от фреймворков.
  • Виджеты для вставки на сторонние сайты (инкапсуляция стилей спасает).
  • Долгоживущие проекты, где важна стабильность нативного стека.
  • Хотите уверенно верстать интерфейсы, понимать каскад, шрифты, сетки и собирать аккуратные компоненты? Самое время прокачаться: загляните в практический курс по вёрстке «Вёрстка сайта с нуля 2.0» — он отлично дополняет тему Web Components и ускорит прогресс.

    Итоги

    Мы разобрали ключевые части Web Components: Custom Elements, Shadow DOM, шаблоны и слоты; подключили стили, атрибуты, события и доступность. Этого достаточно, чтобы начать собирать собственную библиотеку нативных UI-элементов без зависимостей. Дальше можно углубляться в ::part/::theme, adoptedStyleSheets и form-associated элементы. Удачной разработки!

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий