WEB-COMPONENTS CSS STYLING THEMING DESIGN-SYSTEM FRONTEND CUSTOM-PROPERTIES SHADOW-DOM UI-UX DEVELOPER-EXPERIENCE WEB-STANDARDS

Membuat Komponen Web yang Fleksibel dan Mudah Ditemakan: Menguasai CSS Custom Properties dan ::part Pseudo-element

⏱️ 25 menit baca
👨‍💻

Membuat Komponen Web yang Fleksibel dan Mudah Ditemakan: Menguasai CSS Custom Properties dan ::part Pseudo-element

1. Pendahuluan

Sebagai developer web, kita sering dihadapkan pada kebutuhan untuk membangun komponen UI yang reusable dan fleksibel. Bayangkan Anda membuat sebuah komponen tombol (<my-button>) yang ingin digunakan di berbagai bagian aplikasi, atau bahkan di berbagai proyek dengan tema visual yang berbeda. Bagaimana Anda bisa memastikan tombol tersebut mudah disesuaikan warnanya, ukuran teksnya, atau bahkan border-nya tanpa harus mengubah kode inti komponen itu sendiri?

Inilah tantangan utama dalam membangun sistem desain dan komponen yang kuat. Web Components dengan Shadow DOM-nya memberikan isolasi gaya yang luar biasa, mencegah style leakage dan konflik CSS. Namun, isolasi ini juga bisa menjadi pedang bermata dua: bagaimana cara kita “menembus” Shadow DOM untuk memberikan sentuhan gaya dari luar?

Jangan khawatir! Web platform telah menyediakan dua jurus ampuh untuk mengatasi masalah ini: CSS Custom Properties (Variabel CSS) dan ::part pseudo-element. Dalam artikel ini, kita akan menyelami kedua fitur ini, memahami cara kerjanya, dan bagaimana menggunakannya secara efektif untuk menciptakan Web Components yang benar-benar fleksibel dan mudah ditemakan.

🎯 Tujuan kita adalah membangun komponen yang powerful di dalamnya, namun adaptif di luarnya.

2. Sekilas tentang Web Components dan Shadow DOM

Sebelum kita melangkah lebih jauh, mari kita ingat kembali mengapa styling Web Components bisa jadi tricky.

Web Components terdiri dari beberapa standar, salah satunya adalah Shadow DOM. Shadow DOM memungkinkan kita untuk melampirkan sebuah “pohon DOM” tersembunyi ke sebuah elemen, mengisolasi markup, style, dan perilakunya dari DOM utama.

<!-- index.html -->
<style>
  /* Style ini TIDAK akan mempengaruhi elemen di dalam Shadow DOM */
  button {
    background-color: red;
  }
</style>

<my-button></my-button>

<script>
  class MyButton extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.innerHTML = `
        <style>
          /* Style ini HANYA berlaku di dalam Shadow DOM */
          button {
            background-color: blue;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
          }
        </style>
        <button><slot></slot></button>
      `;
    }
  }
  customElements.define('my-button', MyButton);
</script>

Pada contoh di atas, CSS button { background-color: red; } di DOM utama tidak akan bisa mengubah warna latar belakang tombol di dalam <my-button>, karena style di dalam Shadow DOM memiliki prioritas dan terisolasi. Ini adalah fitur yang bagus untuk menghindari konflik, tetapi bagaimana jika kita ingin mengubah warna biru bawaan menjadi hijau dari luar?

Di sinilah CSS Custom Properties dan ::part pseudo-element berperan.

3. Jurus Pertama: CSS Custom Properties (Variabel CSS) untuk Fleksibilitas Internal

CSS Custom Properties, atau lebih dikenal sebagai Variabel CSS, memungkinkan kita untuk mendefinisikan variabel dalam CSS yang dapat digunakan di mana saja dalam stylesheet. Yang paling menarik, variabel ini bisa menembus Shadow DOM!

📌 Konsep Inti: Custom Properties didefinisikan secara cascade. Artinya, jika Anda mendefinisikan sebuah custom property pada elemen induk (di luar Shadow DOM), nilai tersebut akan diwarisi oleh elemen-elemen di dalamnya, termasuk yang berada di Shadow DOM.

Cara Kerja

  1. Definisikan Custom Property di dalam Shadow DOM: Gunakan sebagai default value atau fallback.
  2. Gunakan Custom Property di dalam Shadow DOM: Terapkan ke elemen internal.
  3. Override Custom Property dari luar Shadow DOM: Ubah nilainya pada host element atau elemen induknya.

Contoh Konkret: Tombol yang Mudah Ditemakan

Mari kita modifikasi komponen <my-button> kita agar warnanya bisa diubah dari luar.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Theming Web Components</title>
    <style>
        /* Mengubah warna tombol secara global */
        body {
            --my-button-bg-color: #4CAF50; /* Hijau sebagai default global */
            --my-button-text-color: white;
            font-family: sans-serif;
            padding: 20px;
        }

        /* Mengubah warna tombol untuk instance spesifik */
        .primary-button {
            --my-button-bg-color: #007bff; /* Biru */
        }

        .danger-button {
            --my-button-bg-color: #dc3545; /* Merah */
            --my-button-text-color: yellow; /* Override teks juga */
        }
    </style>
</head>
<body>
    <h1>Contoh Theming dengan Custom Properties</h1>

    <my-button>Default Button</my-button>
    <my-button class="primary-button">Primary Button</my-button>
    <my-button class="danger-button">Danger Button</my-button>
    <my-button style="--my-button-bg-color: purple;">Purple Button</my-button>

    <script>
        class MyButton extends HTMLElement {
            constructor() {
                super();
                const shadowRoot = this.attachShadow({ mode: 'open' });
                shadowRoot.innerHTML = `
                    <style>
                        button {
                            /* Menggunakan custom properties dengan fallback */
                            background-color: var(--my-button-bg-color, #007bff); /* Default biru jika tidak di-override */
                            color: var(--my-button-text-color, white); /* Default putih */
                            padding: 10px 20px;
                            border: none;
                            border-radius: 5px;
                            cursor: pointer;
                            font-size: 16px;
                            margin: 5px;
                            transition: background-color 0.3s ease;
                        }

                        button:hover {
                            filter: brightness(1.1);
                        }
                    </style>
                    <button><slot></slot></button>
                `;
            }
        }
        customElements.define('my-button', MyButton);
    </script>
</body>
</html>

💡 Penjelasan:

Kelebihan Custom Properties:

Kekurangan Custom Properties:

4. Jurus Kedua: Menguasai ::part Pseudo-element untuk Kontrol Eksternal yang Presisi

Bagaimana jika Anda ingin mengontrol styling elemen internal yang lebih spesifik, seperti menambahkan border ke teks di dalam tombol, atau mengubah display-nya? Di sinilah ::part pseudo-element bersinar.

::part memungkinkan kita untuk secara eksplisit “mengekspos” bagian-bagian internal dari Shadow DOM agar bisa di-style dari luar.

📌 Konsep Inti: Anda menandai elemen internal di Shadow DOM dengan atribut part. Kemudian, dari luar Shadow DOM, Anda bisa menargetkan elemen-elemen ini menggunakan ::part(nama-part).

Cara Kerja

  1. Tandai Elemen Internal: Tambahkan atribut part="nama-part" ke elemen di dalam Shadow DOM yang ingin Anda ekspos.
  2. Style dari Luar: Gunakan nama-komponen::part(nama-part) di stylesheet DOM utama untuk menargetkan dan men-style elemen tersebut.

Contoh Konkret: Tombol dengan Teks yang Bisa Di-style

Mari kita buat tombol yang teksnya bisa kita beri border dari luar.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Theming Web Components dengan ::part</title>
    <style>
        body {
            font-family: sans-serif;
            padding: 20px;
        }

        /* Styling elemen internal 'label' dari luar */
        my-button-part::part(label) {
            text-transform: uppercase;
            font-weight: bold;
            letter-spacing: 1px;
            color: darkblue; /* Override warna teks */
            border: 2px solid lightblue; /* Tambahkan border */
            padding: 2px 5px;
            border-radius: 3px;
        }

        /* Styling tombol secara keseluruhan */
        my-button-part {
            background-color: #f0f0f0; /* Warna latar belakang host element */
            padding: 10px;
            border-radius: 8px;
            display: inline-block; /* Agar padding dan background terlihat */
            margin: 5px;
        }
    </style>
</head>
<body>
    <h1>Contoh Theming dengan ::part Pseudo-element</h1>

    <my-button-part>Klik Saya</my-button-part>
    <my-button-part>Submit Data</my-button-part>

    <script>
        class MyButtonPart extends HTMLElement {
            constructor() {
                super();
                const shadowRoot = this.attachShadow({ mode: 'open' });
                shadowRoot.innerHTML = `
                    <style>
                        button {
                            background-color: #007bff;
                            color: white;
                            padding: 10px 20px;
                            border: none;
                            border-radius: 5px;
                            cursor: pointer;
                            font-size: 16px;
                            display: flex; /* Untuk menata label dan icon */
                            align-items: center;
                            gap: 8px;
                        }

                        /* Menandai elemen span dengan part="label" */
                        span[part="label"] {
                            /* Style default untuk label */
                            color: white;
                            font-family: inherit;
                        }

                        button:hover {
                            filter: brightness(1.1);
                        }
                    </style>
                    <button>
                        <span part="label"><slot></slot></span>
                        <!-- Anda juga bisa mengekspos bagian lain, misalnya icon -->
                        <!-- <span part="icon">⚙️</span> -->
                    </button>
                `;
            }
        }
        customElements.define('my-button-part', MyButtonPart);
    </script>
</body>
</html>

💡 Penjelasan:

Kelebihan ::part:

Kekurangan ::part:

5. Kombinasi Kekuatan: Custom Properties dan ::part untuk Theming Lanjutan

Seringkali, solusi terbaik adalah menggabungkan kedua jurus ini.

Contoh: Komponen Card yang Fleksibel

Bayangkan kita punya komponen <my-card> dengan header, body, dan footer.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Advanced Web Component Theming</title>
    <style>
        body {
            font-family: sans-serif;
            padding: 20px;
            background-color: #f4f4f4;
        }

        /* Global theme via Custom Properties */
        :root {
            --card-border-radius: 8px;
            --card-header-bg: #3498db; /* Biru */
            --card-header-text-color: white;
            --card-body-padding: 15px;
            --card-footer-bg: #ecf0f1; /* Abu-abu muda */
        }

        /* Specific styling for a card instance */
        .featured-card {
            --card-header-bg: #e74c3c; /* Merah */
            --card-header-text-color: yellow;
            border: 2px solid #e74c3c;
        }

        /* Styling a specific part of the card's header */
        my-card.featured-card::part(header) {
            border-bottom: 3px dashed yellow;
        }

        /* Styling a specific part of the card's body */
        my-card::part(body) {
            font-size: 1.1em;
            line-height: 1.6;
        }
    </style>
</head>
<body>
    <h1>Theming Lanjutan: Kombinasi Custom Properties dan ::part</h1>

    <my-card>
        <h2 slot="header">Judul Card Default</h2>
        <p slot="body">Ini adalah konten utama dari card. Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
        <button slot="footer">Aksi</button>
    </my-card>

    <my-card class="featured-card">
        <h2 slot="header">Featured Card</h2>
        <p slot="body">Konten spesial untuk card yang di-highlight.</p>
        <button slot="footer">Baca Selengkapnya</button>
    </my-card>

    <script>
        class MyCard extends HTMLElement {
            constructor() {
                super();
                const shadowRoot = this.attachShadow({ mode: 'open' });
                shadowRoot.innerHTML = `
                    <style>
                        :host {
                            display: block;
                            border: 1px solid #ccc;
                            border-radius: var(--card-border-radius, 5px);
                            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
                            margin: 10px;
                            width: 300px;
                            overflow: hidden;
                            background-color: white;
                        }

                        .card-header {
                            background-color: var(--card-header-bg, #f3f3f3);
                            color: var(--card-header-text-color, #333);
                            padding: 10px var(--card-body-padding, 15px);
                            font-size: 1.2em;
                        }

                        .card-body {
                            padding: var(--card-body-padding, 15px);
                        }

                        .card-footer {
                            background-color: var(--card-footer-bg, #eee);
                            padding: 10px var(--card-body-padding, 15px);
                            text-align: right;
                        }
                    </style>
                    <div class="card-header" part="header">
                        <slot name="header"></slot>
                    </div>
                    <div class="card-body" part="body">
                        <slot name="body"></slot>
                    </div>
                    <div class="card-footer" part="footer">
                        <slot name="footer"></slot>
                    </div>
                `;
            }
        }
        customElements.define('my-card', MyCard);
    </script>
</body>
</html>

Dalam contoh <my-card> ini:

Kombinasi ini memberikan fleksibilitas maksimal: Custom Properties untuk nilai-nilai yang bisa diwarisi, dan ::part untuk area-area spesifik yang bisa dijangkau oleh style eksternal.

6. Best Practices dan Pertimbangan Pent