WEB-COMPONENTS FRONTEND WEB-DEVELOPMENT SHADOW-DOM SLOTS COMPONENT-DEVELOPMENT STYLING CSS ENCAPSULATION ARCHITECTURE BEST-PRACTICES UI-UX HTML-TEMPLATES

Membangun Komponen Web yang Fleksibel dan Terkomposisi: Menguasai Slots, Shadow DOM, dan Styling Lanjutan

⏱️ 15 menit baca
👨‍💻

Membangun Komponen Web yang Fleksibel dan Terkomposisi: Menguasai Slots, Shadow DOM, dan Styling Lanjutan

1. Pendahuluan

Sebagai developer web, kita selalu berusaha membangun aplikasi yang efisien, mudah dirawat, dan memiliki user interface (UI) yang konsisten. Salah satu fondasi untuk mencapai ini adalah dengan membangun komponen UI yang reusable (dapat digunakan kembali). Di sinilah Web Components hadir sebagai solusi standar web yang kuat.

Anda mungkin sudah akrab dengan konsep dasar Web Components: Custom Elements untuk mendefinisikan elemen HTML kustom Anda, dan Shadow DOM untuk mengisolasi struktur dan gaya komponen. Namun, tantangan sering muncul ketika kita ingin membuat komponen yang benar-benar fleksibel dan terkomposisi. Bagaimana cara menyuntikkan konten dinamis ke dalamnya? Dan bagaimana kita bisa men-style komponen yang terisolasi tersebut tanpa melanggar batas enkapsulasi Shadow DOM?

Artikel ini akan membawa Anda lebih dalam. Kita akan menjelajahi kekuatan slots untuk komposisi konten yang dinamis dan strategi styling Shadow DOM tingkat lanjut, termasuk penggunaan ::slotted(), CSS Custom Properties, dan ::part(). Bersiaplah untuk membangun Web Components yang tidak hanya reusable, tetapi juga sangat adaptif dan mudah di-style! 🎯

2. Web Components: Sekilas Mengenai Fondasi Komponen Web Modern

Sebelum kita menyelam lebih dalam, mari kita refresh sedikit tentang Web Components. Ini adalah set standar teknologi web yang memungkinkan Anda membuat elemen HTML yang dapat digunakan kembali, terenkapsulasi, dan framework-agnostic. Ada tiga pilar utama:

Konsep Shadow DOM adalah kunci untuk enkapsulasi, namun ia juga yang seringkali menjadi sumber kebingungan saat membahas styling dan komposisi konten.

3. Memahami Kekuatan Slots untuk Komposisi Konten

slots adalah salah satu fitur paling powerful dari Web Components untuk membuat komponen yang fleksibel. Mereka bertindak sebagai placeholder di dalam Shadow DOM komponen Anda, di mana konten dari luar komponen (disebut light DOM) dapat disuntikkan.

📌 Default Slot: Menyuntikkan Konten Umum

Setiap elemen <slot> tanpa atribut name adalah default slot. Semua konten yang Anda tempatkan di dalam Custom Element Anda yang tidak cocok dengan named slot akan masuk ke default slot ini.

Contoh: Komponen <my-card> sederhana.

<!-- my-card.js -->
<template id="my-card-template">
  <style>
    .card {
      border: 1px solid #ccc;
      padding: 16px;
      margin: 8px;
      border-radius: 8px;
      box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
    }
  </style>
  <div class="card">
    <slot></slot> <!-- Default slot -->
  </div>
</template>

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

<!-- Penggunaan -->
<my-card>
  <h2>Judul Kartu Saya</h2>
  <p>Ini adalah konten utama dari kartu.</p>
</my-card>

💡 Dengan default slot, Anda bisa menyuntikkan berbagai jenis konten ke dalam komponen, menjadikannya sangat fleksibel untuk kasus penggunaan umum.

📌 Named Slots: Komposisi Konten yang Lebih Spesifik

Ketika Anda membutuhkan lebih banyak kontrol atas penempatan konten di dalam komponen, Anda bisa menggunakan named slots. Setiap <slot> dengan atribut name akan menjadi named slot. Konten yang ingin Anda suntikkan harus memiliki atribut slot yang sesuai dengan name dari slot target.

Contoh: Mengembangkan <my-card> dengan slot untuk header, body, dan footer.

<!-- my-card.js (Template diupdate) -->
<template id="my-card-template">
  <style>
    .card {
      border: 1px solid #ccc;
      padding: 16px;
      margin: 8px;
      border-radius: 8px;
      box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
      display: flex;
      flex-direction: column;
    }
    .card-header {
      font-weight: bold;
      margin-bottom: 8px;
      border-bottom: 1px solid #eee;
      padding-bottom: 8px;
    }
    .card-footer {
      margin-top: 16px;
      border-top: 1px solid #eee;
      padding-top: 8px;
      font-size: 0.9em;
      color: #777;
    }
  </style>
  <div class="card">
    <div class="card-header">
      <slot name="header">Default Header</slot> <!-- Named slot for header -->
    </div>
    <div class="card-body">
      <slot>Default Body Content</slot> <!-- Default slot for body -->
    </div>
    <div class="card-footer">
      <slot name="footer">Default Footer</slot> <!-- Named slot for footer -->
    </div>
  </div>
</template>

<script>
  // ... (MyCard class sama seperti sebelumnya)
</script>

<!-- Penggunaan -->
<my-card>
  <h2 slot="header">Laporan Penjualan</h2>
  <p>Penjualan Q3 meningkat 15% dari kuartal sebelumnya.</p>
  <button slot="footer">Lihat Detail</button>
</my-card>

<my-card>
  <!-- Hanya mengisi body, header dan footer akan menggunakan default -->
  <p>Ini adalah kartu tanpa header dan footer kustom.</p>
</my-card>

✅ Dengan named slots, Anda bisa menciptakan komponen yang memiliki “area” yang jelas untuk konten yang berbeda, memberikan struktur dan fleksibilitas yang luar biasa. Jika slot tidak diisi, konten default di dalam <slot> akan ditampilkan.

4. Shadow DOM: Isolasi yang Kuat, Fleksibilitas Styling yang Tersembunyi

Shadow DOM adalah fitur Web Components yang paling penting untuk enkapsulasi. Ketika Anda melampirkan Shadow DOM ke sebuah elemen (misalnya, this.attachShadow({ mode: 'open' })), Anda menciptakan sebuah “pulau” DOM dan CSS yang terisolasi.

Manfaat Enkapsulasi:

Namun, isolasi ini juga membawa tantangan: bagaimana jika kita ingin men-style komponen Web Component dari luar? Atau bagaimana jika kita ingin men-style konten yang disuntikkan melalui slots? Di sinilah teknik styling lanjutan berperan.

5. Strategi Styling Lanjutan di Balik Shadow Boundary

Meskipun Shadow DOM mengisolasi style, standar Web Components menyediakan cara-cara yang terkontrol untuk berinteraksi dengan styling lintas batas Shadow DOM.

📌 Styling Host Element dengan :host() dan :host-context()

Elemen host adalah Custom Element itu sendiri (<my-card> dalam contoh kita). Anda bisa men-style elemen host dari dalam Shadow DOM menggunakan pseudo-class :host.

💡 Ini memungkinkan komponen untuk bereaksi terhadap lingkungannya atau state-nya sendiri, memberikan fleksibilitas tanpa membocorkan style internal.

📌 Styling Konten yang Dislot dengan ::slotted()

Ketika konten dari light DOM disuntikkan ke dalam slot, ia mempertahankan style aslinya. Namun, Anda bisa men-style elemen-elemen ini dari dalam Shadow DOM komponen menggunakan pseudo-element ::slotted().

Penting: ::slotted() hanya bisa menargetkan direct children dari slot. Anda tidak bisa menargetkan elemen cucu atau lebih dalam.

Contoh: Men-style elemen <h2> dan <p> yang dislot ke dalam <my-card>.

<!-- my-card.js (Template diupdate) -->
<template id="my-card-template">
  <style>
    /* ... style .card, .card-header, .card-footer ... */

    /* Men-style elemen <h2> yang langsung dislot ke default slot */
    ::slotted(h2) {
      color: #333;
      font-size: 1.8em;
      margin-top: 0;
    }

    /* Men-style elemen <p> yang langsung dislot ke default slot */
    ::slotted(p) {
      color: #555;
      line-height: 1.6;
    }

    /* Men-style tombol yang dislot ke footer slot */
    ::slotted([slot="footer"]) { /* Target elemen apapun dengan slot="footer" */
      background-color: #007bff;
      color: white;
      border: none;
      padding: 8px 12px;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
  <!-- ... struktur HTML dengan slots ... -->
</template>

⚠️ Ingat batasan ::slotted()! Jika Anda perlu men-style elemen yang lebih dalam, Anda mungkin perlu menggunakan CSS Custom Properties atau Shadow Parts.

📌 CSS Custom Properties: Jembatan Styling Lintas Shadow DOM

CSS Custom Properties (atau CSS Variables) adalah jurus rahasia untuk theming dan styling yang fleksibel lintas batas Shadow DOM. Anda bisa mendefinisikan variabel di luar komponen (atau di elemen host), dan menggunakannya di dalam Shadow DOM.

Contoh: Menggunakan CSS Custom Properties untuk warna latar belakang dan warna teks.

<!-- my-card.js (Template diupdate) -->
<template id="my-card-template">
  <style>
    .card {
      border: 1px solid var(--card-border-color, #ccc); /* Default #ccc */
      padding: 16px;
      margin: 8px;
      border-radius: 8px;
      box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
      background-color: var(--card-bg-color, white); /* Default white */
      color: var(--card-text-color, #333); /* Default #333 */
    }
    .card-header {
      border-bottom-color: var(--card-border-color, #eee);
    }
    .card-footer {
      border-top-color: var(--card-border-color, #eee);
    }
  </style>
  <!-- ... struktur HTML ... -->
</template>

<!-- Penggunaan -->
<style>
  /* Menentukan custom properties untuk semua my-card */
  my-card {
    --card-border-color: #ff0000;
    --card-bg-color: #fffacd;
    --card-text-color: #8b0000;
  }

  /* Atau spesifik untuk satu komponen */
  #mySpecificCard {
    --card-bg-color: #e6f7ff;
    --card-text-color: #0056b3;
  }
</style>

<my-card id="mySpecificCard">
  <h2 slot="header">Info Penting</h2>
  <p>Ini adalah pesan dengan tema kustom.</p>
</my-card>

✅ CSS Custom Properties adalah cara paling direkomendasikan untuk memungkinkan kustomisasi styling dari luar Shadow DOM, karena mereka melewati batas isolasi secara alami.

📌 CSS Shadow Parts: Mengekspos Bagian Internal untuk Styling Eksternal

Untuk skenario di mana Anda perlu men-style elemen internal yang spesifik di dalam Shadow DOM dari luar, dan CSS Custom Properties tidak cukup fleksibel, Anda bisa menggunakan part atribut dan pseudo-element ::part().

Atribut part ditambahkan ke elemen internal di dalam Shadow DOM yang ingin Anda “ekspos” untuk styling. Kemudian, Anda bisa menargetkannya dari luar komponen menggunakan ::part(<nama-part>).

Contoh: Mengekspos header dan footer dari <my-card> agar bisa distyle langsung.

<!-- my-card.js (Template diupdate) -->
<template id="my-card-template">
  <style>
    .card { /* ... style dasar ... */ }
    .card-header { /* ... style dasar ... */ }
    .card-footer { /* ... style dasar ... */ }
  </style>
  <div class="card">
    <div class="card-header" part="header-section"> <!-- Ekspos header -->
      <slot name="header"></slot>
    </div>
    <div class="card-body">
      <slot></slot>
    </div>
    <div class="card-footer" part="footer-section"> <!-- Ekspos footer -->
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<!-- Penggunaan -->
<style>
  my-card::part(header-section) {
    background-color: #f8d7da; /* Warna latar belakang merah muda */
    color: #721c24; /* Warna teks merah gelap */
    padding: 12px;
    border-radius: 4px;
  }

  my-card::part(footer-section) {
    text-align: right;
    font-style: italic;
  }
</style>

<my-card>
  <h2 slot="header">Peringatan Keamanan</h2>
  <p>Perbarui kata sandi Anda untuk keamanan akun yang lebih baik.</p>
  <span slot="footer">Tanggal: 2023-10-26</span>
</my-card>

🎯 ::part() memberikan kontrol styling yang sangat granular dari luar komponen, memungkinkan Anda untuk menargetkan elemen spesifik yang diekspos oleh pembuat komponen. Ini adalah fitur yang relatif baru dan sangat powerful untuk Design System.

6. Studi Kasus: Membangun Komponen Alert yang Fleksibel

Mari kita gabungkan semua konsep ini untuk membangun komponen <my-alert> yang fleksibel.

<!-- my-alert.js -->
<template id="my-alert-template">
  <style>
    :host {
      display: block; /* Agar alert mengambil lebar penuh */
      padding: 15px;
      margin-bottom: 1rem;
      border: 1px solid transparent;
      border-radius: 0.25rem;
      font-family: sans-serif;

      /* CSS Custom Properties dengan fallback */
      background-color: var(--alert-bg-color, #e2e3e5);
      border-color: var(--alert-border-color, #d6d8db);
      color: var(--alert-text-color, #383d41);
    }

    :host([type="success"]) {
      --alert-bg-color: #d4edda;
      --alert-border-color: #c3e6cb;
      --alert-text-color: #155724;
    }

    :host([type="danger"]) {
      --alert-bg-color: #f8d7da;
      --alert-border-color: #f5c6cb;
      --alert-text-color: #721c24;
    }

    .alert-header {
      font-weight: bold;
      margin-bottom: 0.5rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .alert-message {
      line-height: 1.5;
    }

    .alert-actions {
      margin-top: 1rem;
      text-align: right;
    }

    /* Styling slotted content */
    ::slotted(h3) {
      margin-top: 0;
      margin-bottom: 0;
      font-size: 1.25rem;
    }

    ::slotted(button) {
      background-color: var(--alert-action-bg-color, #007bff);
      color: var(--alert-action-text-color, white);
      border: 1px solid var(--alert-action-border-color, #007bff);
      padding: 0.375rem 0.75rem;
      border-radius: 0.25rem;
      cursor: pointer;
    }

    /* Styling part for close button */
    [part="close-button"] {
      background: none;
      border: none;
      font-size: 1.5rem;
      cursor: pointer;
      color: inherit; /* Inherit color from host */
      line-height: 1;
      padding: 0;
    }
  </style>
  <div class="alert">
    <div class="alert-header">
      <slot name="title">Pemberitahuan</slot>
      <button part="close-button">&times;</button> <!-- Close button exposed as a part -->
    </div>
    <div class="alert-message">
      <slot></slot> <!-- Default slot for message -->
    </div>
    <div class="alert-actions">
      <slot name="actions"></slot> <!-- Named slot for action buttons -->
    </div>
  </div>
</template>

<script>
  class MyAlert extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById('my-alert-template');
      const content = template.content.cloneNode(true);
      this.attachShadow({ mode: 'open' }).appendChild(content);

      this.shadowRoot.querySelector('[part="close-button"]').addEventListener('click', () => {
        this.remove(); // Menghapus alert saat tombol close diklik
      });
    }

    static get observedAttributes() {
      return ['type'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'type' && oldValue !== newValue) {
        // Logika tambahan jika diperlukan berdasarkan perubahan type
      }
    }
  }
  customElements.define('my-alert', MyAlert);
</script>

<!-- Penggunaan -->
<style>
  /* Global custom properties untuk alert */
  body {
    --alert-action-bg-color: #28a745;
    --alert-action-border-color: #28a745;
  }
</style>

<my-alert>
  <h3 slot="title">Info Terbaru!</h3>
  <p>Website kami akan melakukan maintenance pada pukul 02:00 WIB.</p>
</my-alert>

<my-alert type="success">
  <h3 slot="title">Berhasil!</h3>
  <p>Data Anda telah berhasil disimpan ke database.</p>
  <button slot="actions">Lihat Data</button>
</my-alert>

<my-alert type="danger">
  <h3 slot="title">Error!</h3>
  <p>Terjadi kesalahan saat memproses permintaan Anda. Silakan coba lagi nanti.</p>
  <button slot="actions">Coba Lagi</button>
  <button slot="actions">Hubungi Admin</button>
</my-alert>

<my-