Web Components – komponenty bez frameworków

Przez ostatnie lata ekosystem JavaScriptu był zdominowany przez frameworki takie jak React, Vue czy Angular. Każdy z nich oferuje własny system komponentów, własny cykl życia i własne abstrakcje. Jednak od dłuższego czasu istnieje coś, co daje nam podobne możliwości bez instalowania ani jednej zależności – Web Components. To natywny standard wbudowany bezpośrednio w przeglądarki, który dojrzał na tyle, że warto mu poświęcić poważną uwagę.

Czym są Web Components?

Web Components to zbiór trzech niezależnych specyfikacji W3C, które razem tworzą kompletny system komponentów:

  • Custom Elements – pozwalają definiować własne tagi HTML wraz z ich zachowaniem
  • Shadow DOM – umożliwia enkapsulację struktury, stylów i logiki komponentu
  • HTML Templates – znaczniki <template> i <slot> do definiowania szablonów wielokrotnego użytku

Wszystkie trzy technologie są obsługiwane przez wszystkie nowoczesne przeglądarki – Chrome, Firefox, Safari, Edge – bez żadnych polyfilli. To ogromna zmiana w porównaniu z sytuacją sprzed kilku lat, gdy wsparcie było fragmentaryczne i problematyczne.

Custom Elements – Twoje własne tagi HTML

Pierwszym filarem Web Components są Custom Elements. Dzięki nim możesz zarejestrować własny element HTML i nadać mu dowolne zachowanie przy użyciu czystego JavaScriptu.

class MojPrzycisk extends HTMLElement {
  constructor() {
    super();
    this.addEventListener('click', () => {
      console.log('Kliknięto!');
    });
  }

  connectedCallback() {
    this.innerHTML = `<button>${this.getAttribute('label') || 'Kliknij mnie'}</button>`;
  }
}

customElements.define('moj-przycisk', MojPrzycisk);

Po zarejestrowaniu takiego elementu możesz go używać w HTML jak każdego innego tagu:

<moj-przycisk label="Zapisz"></moj-przycisk>

Ważna zasada: nazwy Custom Elements muszą zawierać myślnik (np. moj-przycisk, app-header). To celowy zabieg, który pozwala przeglądarce odróżnić elementy natywne od niestandardowych i zapewnia, że przyszłe elementy HTML nigdy nie wejdą w konflikt z Twoimi komponentami.

Lifecycle callbacks

Custom Elements posiadają bogaty zestaw metod cyklu życia:

  • connectedCallback() – wywoływana, gdy element zostaje dodany do DOM
  • disconnectedCallback() – wywoływana, gdy element zostaje usunięty z DOM
  • attributeChangedCallback(name, oldValue, newValue) – wywoływana przy zmianie atrybutu
  • adoptedCallback() – wywoływana, gdy element zostaje przeniesiony do innego dokumentu

Aby attributeChangedCallback działało, należy zadeklarować obserwowane atrybuty:

static get observedAttributes() {
  return ['label', 'disabled'];
}

Shadow DOM – enkapsulacja na poziomie przeglądarki

Shadow DOM to mechanizm, który pozwala tworzyć "ukryte" drzewo DOM wewnątrz elementu, odizolowane od reszty strony. Style zdefiniowane wewnątrz Shadow DOM nie wpływają na zewnętrzny dokument i odwrotnie – style globalne nie "wnikają" do środka komponentu.

class KartaUzytkownika extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        .karta {
          background: #f0f4ff;
          border-radius: 12px;
          padding: 16px;
          font-family: sans-serif;
        }
        h2 { color: #333; margin: 0 0 8px; }
        p { color: #666; margin: 0; }
      </style>
      <div class="karta">
        <h2>Jan Kowalski</h2>
        <p>Frontend Developer</p>
      </div>
    `;
  }
}

customElements.define('karta-uzytkownika', KartaUzytkownika);

Parametr mode: 'open' oznacza, że Shadow DOM jest dostępny z zewnątrz przez element.shadowRoot. Tryb closed całkowicie blokuje zewnętrzny dostęp – przydatne w bibliotekach komponentów, gdzie nie chcemy, aby użytkownicy ingerowali w wewnętrzną strukturę.

HTML Templates i Sloty

Element <template> pozwala zdefiniować fragment HTML, który nie jest renderowany podczas ładowania strony, ale może być wielokrotnie klonowany i wstawiany do DOM:

<template id="szablon-karty">
  <style>
    .karta { border: 1px solid #ddd; padding: 16px; border-radius: 8px; }
  </style>
  <div class="karta">
    <slot name="tytul"></slot>
    <slot name="tresc">Brak treści</slot>
  </div>
</template>

Sloty (<slot>) to miejsca, w których użytkownik komponentu może wstrzyknąć własną treść – to odpowiednik koncepcji "children" znanych z Reacta czy "ng-content" z Angulara. Użycie takiego szablonu:

<moja-karta>
  <h3 slot="tytul">Tytuł karty</h3>
  <p slot="tresc">To jest treść mojej karty.</p>
</moja-karta>

A w klasie komponentu:

connectedCallback() {
  const szablon = document.getElementById('szablon-karty');
  const klon = szablon.content.cloneNode(true);
  this.attachShadow({ mode: 'open' }).appendChild(klon);
}

Komunikacja z komponentem

Jednym z kluczowych aspektów pracy z Web Components jest komunikacja – zarówno "do środka" (przekazywanie danych do komponentu), jak i "na zewnątrz" (reagowanie na zdarzenia komponentu).

Atrybuty i właściwości

Dane przekazujemy do komponentu przez atrybuty HTML (zawsze ciągi znaków) lub przez właściwości JavaScriptowe (mogą być dowolnego typu). Dobrą praktyką jest synchronizowanie obu:

get value() {
  return this._value;
}

set value(val) {
  this._value = val;
  this.setAttribute('value', val);
}

Własne zdarzenia (Custom Events)

Komponent powinien komunikować się z zewnętrznym światem przez zdarzenia:

this.dispatchEvent(new CustomEvent('zmiana-wartosci', {
  detail: { nowaWartosc: this._value },
  bubbles: true,
  composed: true  // ważne! pozwala zdarzeniu przebić się przez Shadow DOM
}));

Parametr composed: true jest kluczowy – bez niego zdarzenie zatrzyma się na granicy Shadow DOM i nie dotrze do elementów nadrzędnych.

Web Components a frameworki – kiedy co wybrać?

To pytanie, które zadaje sobie wielu deweloperów. Odpowiedź, jak to zwykle w programowaniu bywa, brzmi: "to zależy".

Web Components sprawdzają się doskonale, gdy:

  • Tworzysz Design System lub bibliotekę komponentów UI, która ma być używana w różnych projektach – niezależnie od frameworka
  • Chcesz mieć trwałe komponenty odporne na zmiany frameworków (Web Components będą działać za 10 lat)
  • Budujesz aplikacje o niskiej złożoności, gdzie overhead frameworka byłby nieuzasadniony
  • Pracujesz nad projektami CMS-owymi lub stronami, gdzie trzeba wstrzykiwać interaktywność w istniejące HTML
  • Chcesz tworzyć mikrofrontendy, gdzie różne teamsy używają różnych technologii

Frameworki mają przewagę, gdy:

  • Potrzebujesz zaawansowanego zarządzania stanem na poziomie aplikacji
  • Budujesz duże SPA z kompleksowym routingiem
  • Chcesz korzystać z bogatego ekosystemu gotowych narzędzi i bibliotek
  • Twój zespół już dobrze zna dany framework

Warto podkreślić, że Web Components i frameworki nie wykluczają się wzajemnie. React, Vue i Angular doskonale współpracują z natywną specyfikacją – możesz używać Web Components wewnątrz aplikacji frameworkowej.

Lit – gdy czyste Web Components to za mało

Praca z czystym API Web Components bywa czasami rozwlekła i powtarzalna. Dlatego warto wspomnieć o Lit – ultralekkiej bibliotece stworzonej przez Google (wcześniej znana jako LitElement / Polymer), która dodaje wygodne abstrakcje na wierzchu natywnego API.

Lit waży zaledwie ~5KB minified+gzip, a oferuje reaktywne właściwości, efektywny system szablonów i wygodne dekoratory. Przykładowy komponent w Lit:

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('licznik-klikniec')
class LicznikKlikniec extends LitElement {
  static styles = css`
    button { background: #6200ee; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
    span { font-size: 1.5rem; margin-left: 12px; }
  `;

  @property({ type: Number }) licznik = 0;

  render() {
    return html`
      <button @click=${() => this.licznik++}>Kliknij</button>
      <span>${this.licznik}</span>
    `;
  }
}

Lit jest szczególnie polecany do budowania Design Systemów – używają go m.in. Adobe (Spectrum Web Components) czy Google (Material Web).

SEO i dostępność

Jedną z obaw deweloperów jest wpływ Web Components na SEO. Boty Google doskonale radzą sobie z renderowaniem Custom Elements i Shadow DOM – Googlebot wykonuje JavaScript i indeksuje treść. Niemniej jednak, dla krytycznych treści SEO warto zadbać o Server-Side Rendering (SSR) lub Declarative Shadow DOM (DSD) – nowy standard pozwalający serializować Shadow DOM do HTML po stronie serwera.

Jeśli chodzi o dostępność (a11y), Web Components wymagają takiej samej dbałości jak każda inna technologia – poprawne atrybuty ARIA, obsługa klawiatury i kompatybilność z czytnikami ekranowymi leżą w gestii dewelopera, nie w samej specyfikacji.

Podsumowanie

Web Components to dojrzała, stabilna technologia, która rozwiązuje prawdziwy problem – potrzebę tworzenia wielokrotnie używalnych, hermetycznych komponentów UI bez uzależniania się od konkretnego frameworka. Ich największa siłą jest interoperacyjność i długowieczność.

Nie są złotym środkiem na każdy problem i nie zastąpią Reacta czy Vue w budowaniu skomplikowanych aplikacji. Jednak w kontekście Design Systemów, mikrofrontendów czy małych interaktywnych widgetów, Web Components to po prostu najlepsze narzędzie dostępne natywnie w przeglądarce.

Jeśli jeszcze ich nie znasz – czas to zmienić. Wiedza o Web Components to inwestycja, która nie zestarzeją się razem z kolejną wersją ulubionego frameworka.