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
- Definisikan Custom Property di dalam Shadow DOM: Gunakan sebagai default value atau fallback.
- Gunakan Custom Property di dalam Shadow DOM: Terapkan ke elemen internal.
- 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:
- Di dalam Shadow DOM, kita mendefinisikan
background-colordancolortombol menggunakanvar(--my-button-bg-color, #007bff)danvar(--my-button-text-color, white).var()memungkinkan kita untuk menentukan nilai fallback jika custom property tersebut tidak didefinisikan. - Di luar Shadow DOM (
index.html), kita bisa mengubah nilai--my-button-bg-colorpada elemen<body>(mempengaruhi semua instance<my-button>), atau pada instance<my-button>tertentu menggunakan class (.primary-button,.danger-button) atau bahkanstyleinline.
✅ Kelebihan Custom Properties:
- Fleksibilitas Tinggi: Mengontrol banyak aspek styling (warna, font-size, padding, dll.).
- Cascading: Nilai diwarisi, memudahkan theming global.
- Mudah Dipahami: Mirip variabel dalam bahasa pemrograman.
❌ Kekurangan Custom Properties:
- Kontrol Granular Terbatas: Anda hanya bisa mengubah properti yang diekspos oleh komponen. Jika komponen tidak menggunakan custom property untuk suatu elemen internal, Anda tidak bisa mengubahnya dari luar.
- Tidak Bisa Mengubah Struktur: Tidak bisa mengubah display, margin, atau properti lain dari elemen internal secara langsung, hanya nilai yang ditetapkan ke variabel.
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
- Tandai Elemen Internal: Tambahkan atribut
part="nama-part"ke elemen di dalam Shadow DOM yang ingin Anda ekspos. - 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:
- Di dalam Shadow DOM, elemen
<span>yang membungkus slot kita beri atributpart="label". - Di DOM utama, kita bisa menargetkan
my-button-part::part(label)untuk men-style elemen<span>tersebut secara langsung, termasuk properti sepertiborderatautext-transformyang tidak bisa diwarisi oleh Custom Properties.
✅ Kelebihan ::part:
- Kontrol Granular: Memungkinkan styling elemen internal yang sangat spesifik.
- Mengubah Properti Apapun: Anda bisa mengubah properti CSS apa pun pada elemen yang diekspos (misalnya
display,margin,border). - Jelas dan Eksplisit: Komponen secara eksplisit menyatakan bagian mana yang bisa di-style dari luar.
❌ Kekurangan ::part:
- Kurang Fleksibel untuk Theming Global: Anda harus menargetkan setiap part secara spesifik. Tidak ada mekanisme cascading otomatis seperti Custom Properties.
- Markup Lebih Banyak: Membutuhkan penambahan atribut
partdi markup Shadow DOM. - Tidak Bisa Mengubah Child dari Part: Anda hanya bisa men-style elemen yang memiliki atribut
part. Anda tidak bisa men-style anak dari sebuah::partdari luar.
5. Kombinasi Kekuatan: Custom Properties dan ::part untuk Theming Lanjutan
Seringkali, solusi terbaik adalah menggabungkan kedua jurus ini.
- Gunakan Custom Properties untuk aspek-aspek theming yang bersifat global atau mudah diwarisi (misalnya warna primer, warna teks, ukuran font dasar).
- Gunakan
::partuntuk memberikan hook styling yang lebih spesifik pada elemen-elemen internal kunci, memungkinkan developer untuk menyesuaikan aspek layout atau detail visual yang tidak bisa diatasi dengan Custom Properties.
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:
- Kita menggunakan Custom Properties seperti
--card-header-bgdan--card-body-paddinguntuk mengatur warna latar belakang header dan padding secara umum. Ini memungkinkan theming yang mudah di level global (:root) atau per instance. - Kita menggunakan
part="header",part="body", danpart="footer"untuk mengekspos elemen-elemen struktural utama dari card. Ini memungkinkan kita untuk men-styleborder-bottompada header atau mengubahfont-sizepada body dari luar, yang tidak bisa dilakukan hanya dengan Custom Properties.
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.