WEB-COMPONENTS CSS SHADOW-DOM FRONTEND WEB-DEVELOPMENT STYLING DESIGN-SYSTEM CUSTOM-ELEMENTS REUSABILITY FRONTEND-ARCHITECTURE

Styling Web Components: Menguasai CSS di Dalam dan Luar Shadow DOM untuk UI yang Fleksibel

⏱️ 16 menit baca
👨‍💻

Styling Web Components: Menguasai CSS di Dalam dan Luar Shadow DOM untuk UI yang Fleksibel

1. Pendahuluan

Sebagai developer web, kita selalu berusaha membangun UI yang modular, reusable, dan mudah dikelola. Web Components hadir sebagai standar web yang kuat untuk mewujudkan hal itu. Dengan Custom Elements, Shadow DOM, dan HTML Templates, kita bisa membuat komponen UI yang benar-benar terenkapsulasi, terpisah dari kode lain, dan bisa digunakan di mana saja, bahkan lintas framework.

Namun, di balik kekuatan enkapsulasi Shadow DOM, ada satu tantangan yang seringkali membuat developer kebingungan: bagaimana cara men-styling Web Components dengan fleksibel, tanpa merusak isolasi yang menjadi keunggulannya? Bagaimana kita bisa memberikan tema, mengubah warna, atau menyesuaikan ukuran font dari luar komponen, sementara di dalamnya style tetap aman?

Artikel ini akan menjadi panduan praktis Anda untuk menguasai seni styling Web Components. Kita akan menyelami bagaimana Shadow DOM bekerja dengan CSS, dan kemudian membahas berbagai strategi, mulai dari dasar hingga teknik lanjutan, yang memungkinkan Anda membangun komponen yang tidak hanya terenkapsulasi, tetapi juga sangat fleksibel dan dapat dikustomisasi.

Mari kita bongkar misteri styling di dunia Web Components! 🚀

2. Shadow DOM dan Isolasi CSS: Fondasi yang Kuat (dan Sedikit Menantang)

Sebelum kita mulai men-styling, penting untuk memahami kembali konsep inti dari Shadow DOM, terutama hubungannya dengan CSS.

📌 Shadow DOM menciptakan pohon DOM terpisah (disebut Shadow Tree) yang terpasang pada elemen host (Custom Element Anda). Konten di dalam Shadow Tree diisolasi dari DOM utama.

Salah satu fitur paling signifikan dari isolasi ini adalah isolasi CSS.

💡 Mengapa ini penting? Isolasi ini mencegah konflik CSS yang sering terjadi di aplikasi skala besar atau saat mengintegrasikan library pihak ketiga. Setiap komponen Web Component Anda bisa memiliki style-nya sendiri tanpa khawatir tumpang tindih dengan komponen lain.

Contoh Sederhana:

<!-- index.html -->
<style>
  /* CSS global */
  button {
    background-color: red;
    color: white;
  }
</style>

<my-button></my-button>

<script>
  class MyButton extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.innerHTML = `
        <style>
          /* CSS internal Shadow DOM */
          button {
            background-color: blue; /* Ini yang akan dipakai */
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
          }
        </style>
        <button>Klik Saya</button>
      `;
    }
  }
  customElements.define('my-button', MyButton);
</script>

Dalam contoh di atas, tombol di dalam <my-button> akan berwarna biru, bukan merah. Ini menunjukkan bagaimana CSS di dalam Shadow DOM mengesampingkan CSS global untuk elemen di dalamnya, sekaligus mengisolasi style tersebut.

3. Styling Internal: CSS Biasa di Dalam Shadow Root

Cara paling dasar untuk men-styling Web Component Anda adalah dengan menulis CSS langsung di dalam Shadow DOM. Ini adalah praktik yang paling umum dan direkomendasikan untuk style inti komponen.

Anda bisa menempatkan tag <style> langsung di dalam HTML template Shadow DOM Anda:

class SimpleCard extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
      <style>
        /* CSS untuk komponen card ini */
        .card {
          border: 1px solid #ccc;
          border-radius: 8px;
          padding: 16px;
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
          background-color: white;
          font-family: sans-serif; /* Properti yang diwarisi */
          color: #333; /* Properti yang diwarisi */
        }
        .card-title {
          font-size: 1.5em;
          margin-bottom: 8px;
          color: #0056b3;
        }
        .card-content {
          font-size: 0.9em;
          line-height: 1.5;
        }
      </style>
      <div class="card">
        <h3 class="card-title">Judul Kartu</h3>
        <p class="card-content">Ini adalah konten dari kartu sederhana saya.</p>
      </div>
    `;
  }
}
customElements.define('simple-card', SimpleCard);

Keunggulan:

Keterbatasan:

Untuk style inti yang tidak perlu dikustomisasi dari luar, metode ini adalah pilihan terbaik. Namun, bagaimana jika kita ingin memberikan fleksibilitas?

4. Membuka Gerbang Kustomisasi dengan CSS Custom Properties (CSS Variables)

Ini adalah teknik paling ampuh dan direkomendasikan untuk memungkinkan kustomisasi Web Components dari luar Shadow DOM. CSS Custom Properties, atau yang lebih dikenal sebagai CSS Variables, dapat menembus batas Shadow DOM dan memungkinkan elemen induk untuk memengaruhi style di dalamnya.

📌 Cara Kerjanya:

  1. Definisikan Custom Property di dalam Shadow DOM sebagai nilai default untuk properti CSS Anda.
  2. Gunakan Custom Property tersebut di dalam Shadow DOM.
  3. Override Custom Property dari luar komponen (di CSS global atau elemen induk).

Contoh Penggunaan:

// my-themed-button.js
class MyThemedButton extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
      <style>
        button {
          /* Default values jika Custom Property tidak diset dari luar */
          background-color: var(--button-bg, #007bff);
          color: var(--button-color, white);
          padding: var(--button-padding, 10px 20px);
          border: none;
          border-radius: var(--button-border-radius, 5px);
          font-size: var(--button-font-size, 1em);
          cursor: pointer;
        }

        button:hover {
          background-color: var(--button-bg-hover, #0056b3);
        }
      </style>
      <button><slot></slot></button>
    `;
  }
}
customElements.define('my-themed-button', MyThemedButton);
<!-- index.html -->
<style>
  /* Mengatur tema global */
  body {
    --button-bg: #28a745; /* hijau */
    --button-color: black;
    --button-font-size: 1.2em;
  }

  /* Mengatur tema spesifik untuk instance tertentu */
  .dark-theme-button {
    --button-bg: #343a40; /* abu-abu gelap */
    --button-color: #f8f9fa;
    --button-border-radius: 10px;
  }
</style>

<my-themed-button>Tombol Default</my-themed-button>
<my-themed-button class="dark-theme-button">Tombol Tema Gelap</my-themed-button>
<my-themed-button style="--button-bg: purple; --button-color: yellow;">Tombol Kustom</my-themed-button>

🎯 Best Practices dengan Custom Properties:

Custom Properties adalah jembatan utama antara enkapsulasi Shadow DOM dan kebutuhan kustomisasi UI modern.

5. Menggunakan ::part untuk Menargetkan Elemen Internal

Meskipun CSS Custom Properties sangat fleksibel, ada kalanya Anda ingin mengubah properti CSS yang tidak bisa dikontrol oleh Custom Property (misalnya, display, position), atau Anda ingin menargetkan elemen tertentu di dalam Shadow DOM secara langsung dari luar. Di sinilah pseudo-elemen ::part() berperan.

::part() memungkinkan Anda untuk mengekspos bagian-bagian tertentu dari Shadow DOM ke dunia luar, sehingga mereka dapat di-styling.

📌 Cara Kerjanya:

  1. Tambahkan atribut part pada elemen di dalam Shadow DOM yang ingin Anda ekspos.
  2. Gunakan pseudo-elemen ::part() di CSS global atau induk untuk menargetkan elemen tersebut.

Contoh Penggunaan:

// my-fancy-card.js
class MyFancyCard extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
      <style>
        .wrapper {
          border: 1px solid var(--card-border-color, #ccc);
          border-radius: 8px;
          padding: 16px;
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
          background-color: var(--card-bg, white);
        }
        .header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 10px;
          border-bottom: 1px solid var(--card-header-border, #eee);
          padding-bottom: 10px;
        }
        .title {
          font-size: 1.5em;
          color: var(--card-title-color, #333);
        }
        .action-button {
          background-color: var(--action-button-bg, #007bff);
          color: white;
          padding: 5px 10px;
          border: none;
          border-radius: 3px;
          cursor: pointer;
        }
      </style>
      <div class="wrapper" part="card-wrapper">
        <div class="header" part="card-header">
          <h3 class="title" part="card-title"></h3>
          <button class="action-button" part="card-action-button"><slot name="action"></slot></button>
        </div>
        <div class="content" part="card-content">
          <slot></slot>
        </div>
      </div>
    `;
  }

  connectedCallback() {
    this.shadowRoot.querySelector('.title').textContent = this.getAttribute('title') || 'Default Title';
  }
}
customElements.define('my-fancy-card', MyFancyCard);
<!-- index.html -->
<style>
  my-fancy-card::part(card-wrapper) {
    background-color: #f8f9fa;
    border-color: #007bff;
  }
  my-fancy-card::part(card-title) {
    font-family: 'Segoe UI', sans-serif;
    color: #0056b3;
    text-transform: uppercase;
  }
  my-fancy-card::part(card-action-button) {
    background-color: #dc3545; /* Merah */
    font-weight: bold;
  }
</style>

<my-fancy-card title="Produk Baru">
  <span slot="action">Detail</span>
  <p>Deskripsi singkat tentang produk terbaru kami yang inovatif.</p>
</my-fancy-card>

⚠️ Penting:

6. Styling Konten yang Diproyeksikan dengan ::slotted()

Web Components seringkali menggunakan <slot> untuk memproyeksikan konten dari luar komponen ke dalam Shadow DOM. Konten ini, meskipun ditampilkan di dalam Shadow DOM, sebenarnya “hidup” di Light DOM (DOM utama). Ini berarti style dari Shadow DOM tidak akan memengaruhi konten yang diproyeksikan, dan style global akan memengaruhinya.

Namun, Anda bisa men-styling konten yang diproyeksikan dari dalam Shadow DOM menggunakan pseudo-elemen ::slotted().

📌 Cara Kerjanya:

Contoh Penggunaan:

// my-slot-card.js
class MySlotCard extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
      <style>
        .card-container {
          border: 1px solid #ddd;
          padding: 20px;
          border-radius: 10px;
          background-color: #f9f9f9;
        }

        /* Men-styling elemen H2 yang diproyeksikan ke slot default */
        ::slotted(h2) {
          color: #28a745; /* Hijau */
          font-size: 2em;
          border-bottom: 2px solid #28a745;
          padding-bottom: 10px;
          margin-top: 0;
        }

        /* Men-styling elemen P yang diproyeksikan ke slot default */
        ::slotted(p) {
          color: #555;
          line-height: 1.6;
        }

        /* Men-styling elemen apapun yang diproyeksikan ke slot 'footer' */
        ::slotted([slot="footer"]) {
          margin-top: 20px;
          padding-top: 10px;
          border-top: 1px dashed #ccc;
          text-align: right;
          font-style: italic;
        }
      </style>
      <div class="card-container">
        <slot></slot> <!-- Slot default -->
        <slot name="footer"></slot> <!-- Slot bernama 'footer' -->
      </div>
    `;
  }
}
customElements.define('my-slot-card', MySlotCard);
<!-- index.html -->
<my-slot-card>
  <h2>Halo Dunia Web Components!</h2>
  <p>Ini adalah paragraf pertama yang diproyeksikan ke dalam slot default.</p>
  <p>Paragraf kedua juga akan di-styling dengan aturan yang sama.</p>
  <div slot="footer">
    <span>Hak Cipta &copy; 2023</span>
  </div>
</my-slot-card>

💡 Tips:

7. Integrasi dengan Framework CSS (Studi Kasus: Tailwind CSS)

Bagaimana jika Anda ingin menggunakan framework CSS utility-first seperti Tailwind CSS di dalam Web Components Anda? Ini adalah pertanyaan umum, dan ada beberapa pendekatan:

Pendekatan 1: Kompilasi Tailwind CSS ke dalam Shadow DOM (Paling Enkapsulasi)

Ini adalah cara paling “pure” untuk menggunakan Tailwind CSS dengan Web Components. Anda akan mengkompilasi Tailwind CSS untuk setiap komponen atau sekelompok komponen, dan menginjeksikan CSS yang dihasilkan langsung ke dalam Shadow DOM.

// my-tailwind-card.js
// Asumsikan Anda memiliki 'tailwind.css' yang sudah dikompilasi
// atau Anda bisa mengkompilasinya secara on-the-fly (lebih kompleks)
import tailwindStyles from './tailwind-output.css'; // Ini adalah CSS hasil kompilasi Tailwind

class MyTailwindCard extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
      <style>
        ${tailwindStyles} /* Injeksikan seluruh CSS Tailwind yang dibutuhkan */

        /* Atau, lebih baik lagi, hanya CSS yang benar-benar digunakan oleh komponen ini */
        /* Anda bisa menggunakan @apply atau JIT mode untuk mengekstrak hanya yang relevan */
        .card-custom-styles {
          /* Beberapa style kustom jika diperlukan */
        }
      </style>
      <div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
        <div class="shrink-0">
          <img class="h-12 w-12" src="logo.svg" alt="ChitChat Logo">
        </div>
        <div>
          <div class="text-xl font-medium text-black">ChitChat</div>
          <p class="text-slate-500">You have a new message!</p>
        </div>
      </div>
    `;
  }
}
customElements.define('my-tailwind-card', MyTailwindCard);

Cara kerja: Anda perlu mengatur proses build Anda agar Tailwind CSS hanya mengkompilasi kelas-kelas yang digunakan di template Shadow DOM komponen Anda (menggunakan PurgeCSS atau JIT mode). CSS yang dihasilkan kemudian diinjeksikan.

Keunggulan:

Keterbatasan:

Pendekatan 2: Mengandalkan Tailwind CSS Global (Kurang Enkapsulasi)

Jika Anda bersedia mengorbankan sebagian enkapsulasi, Anda bisa mem