CSS Custom State (:state()): Membangun Web Components yang Lebih Interaktif dan Terkontrol
1. Pendahuluan 💡
Sebagai developer web, kita selalu mencari cara untuk membangun antarmuka pengguna (UI) yang lebih modular, efisien, dan mudah dikelola. Web Components telah menjadi fondasi yang kuat untuk mencapai tujuan ini, memungkinkan kita menciptakan elemen HTML kustom yang bisa digunakan ulang di mana saja. Namun, salah satu tantangan yang sering muncul adalah bagaimana men-style Web Component berdasarkan status internalnya secara elegan, terutama dari luar Shadow DOM.
Bayangkan Anda memiliki komponen <my-button> yang bisa berada dalam status “loading”. Secara tradisional, Anda mungkin akan menambahkan atribut seperti <my-button loading> atau class seperti <my-button class="is-loading">. Meskipun cara ini bekerja, ia terasa sedikit “kotor” karena memerlukan JavaScript untuk terus-menerus memanipulasi DOM (menambah/menghapus atribut atau class) hanya untuk tujuan styling. Ini bisa memengaruhi performa dan membuat kode lebih sulit dibaca karena logika presentasi bercampur dengan logika bisnis.
Di sinilah CSS Custom State (:state()) datang sebagai solusi yang revolusioner! Fitur CSS modern ini memungkinkan Web Component untuk mengekspos status internalnya dalam bentuk pseudo-class kustom, yang kemudian dapat ditargetkan oleh CSS. Ini membuka pintu bagi styling yang lebih semantik, performa yang lebih baik, dan pemisahan concern yang lebih jelas antara JavaScript dan CSS. Mari kita selami lebih dalam!
2. Memahami Masalah Styling Web Components Tradisional ⚠️
Sebelum :state() hadir, men-style Web Component berdasarkan statusnya seringkali terasa seperti melakukan hack. Mari kita ambil contoh tombol kustom yang bisa berubah menjadi status “loading”.
Skenario Tradisional:
Kita ingin membuat tombol kustom <loading-button> yang menampilkan indikator loading saat proses sedang berjalan.
<!-- HTML -->
<loading-button>Kirim Data</loading-button>
// JavaScript (loading-button.js)
class LoadingButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<button><slot></slot></button>`;
this.isLoading = false;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.startLoading();
});
}
startLoading() {
if (this.isLoading) return;
this.isLoading = true;
this.setAttribute('loading', ''); // ❌ Manipulasi atribut untuk styling
this.shadowRoot.querySelector('button').disabled = true;
// Simulasikan proses asynchronous
setTimeout(() => {
this.stopLoading();
}, 2000);
}
stopLoading() {
this.isLoading = false;
this.removeAttribute('loading'); // ❌ Manipulasi atribut untuk styling
this.shadowRoot.querySelector('button').disabled = false;
}
}
customElements.define('loading-button', LoadingButton);
/* CSS (dari luar Shadow DOM) */
loading-button[loading] button { /* ⚠️ Menargetkan atribut */
background-color: #cccccc;
cursor: not-allowed;
opacity: 0.7;
}
loading-button[loading]::after { /* ⚠️ Menambahkan teks loading */
content: "...";
margin-left: 5px;
}
Kekurangan Pendekatan Ini:
- Kopling Erat (Tight Coupling): Logika JavaScript perlu tahu tentang atribut
loadingyang sebenarnya hanya untuk tujuan styling. - Manipulasi DOM Berlebihan: Setiap perubahan status yang memengaruhi tampilan memerlukan perubahan atribut/class di DOM, yang bisa memicu layout reflow dan repaint, memengaruhi performa terutama pada aplikasi kompleks.
- Kurang Semantik: Atribut
loadingbisa bentrok dengan atribut lain atau tidak jelas apakah itu untuk logika atau styling. - Potensi Konflik: Jika ada beberapa script yang memanipulasi atribut/class yang sama.
Kita butuh cara yang lebih baik, bukan? Cara di mana komponen bisa “mengumumkan” statusnya tanpa harus secara langsung mengubah representasi DOM untuk styling.
3. Apa itu CSS Custom State (:state())? 📌
CSS Custom State, yang diwakili oleh pseudo-class :state(), adalah sebuah mekanisme yang memungkinkan custom element untuk mengekspos status internalnya ke CSS sebagai pseudo-class kustom. Ini berarti custom element dapat memberi tahu browser bahwa ia sedang dalam status tertentu (misalnya, loading, error, checked, expanded), dan CSS kemudian dapat menargetkan status tersebut menggunakan sintaks :state(nama-state-kustom).
Fitur ini merupakan bagian dari spesifikasi HTML ElementInternals, yang dirancang untuk memungkinkan custom element berinteraksi lebih dalam dengan browser, terutama dalam konteks form-associated custom elements. Namun, kemampuan states di ElementInternals bisa digunakan secara lebih luas untuk tujuan styling.
Bagaimana cara kerjanya secara teknis?
ElementInternals: Setiap custom element dapat mengakses objekElementInternalsmelaluithis.attachInternals()di konstruktornya.statesSet: ObjekElementInternalsmemiliki propertistatesyang merupakan sebuahSet. Anda bisa menambahkan atau menghapus nama state kustom ke/dari Set ini.- Pseudo-class CSS: Ketika Anda menambahkan sebuah string (nama state) ke
this.internals.states, browser akan mengekspos state tersebut sebagai pseudo-class CSS yang bisa ditargetkan menggunakan:state(nama-state).
Ini menciptakan decoupling yang indah: JavaScript komponen hanya perlu mengelola statusnya di ElementInternals.states, dan CSS secara independen dapat bereaksi terhadap perubahan status tersebut.
4. Implementasi Praktis: Membangun Tombol “Loading” dengan :state() 💻
Mari kita revisi contoh tombol “loading” kita menggunakan CSS Custom State.
<!-- index.html -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo CSS Custom State</title>
<style>
/* CSS dari luar Shadow DOM */
my-button {
display: inline-block; /* Agar bisa men-style seperti button */
border: 1px solid #007bff;
padding: 8px 16px;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
font-family: sans-serif;
}
/* Styling saat tombol dalam state 'loading' */
my-button:state(loading) {
background-color: #cccccc;
border-color: #999999;
cursor: not-allowed;
opacity: 0.8;
position: relative;
}
/* Menambahkan indikator loading */
my-button:state(loading)::after {
content: ' ';
display: inline-block;
width: 1em;
height: 1em;
border: 2px solid #fff;
border-top-color: transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
vertical-align: middle;
margin-left: 0.5em;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Styling saat tombol dalam state 'success' */
my-button:state(success) {
background-color: #28a745;
border-color: #28a745;
}
/* Styling saat tombol dalam state 'error' */
my-button:state(error) {
background-color: #dc3545;
border-color: #dc3545;
}
my-button button { /* Menghilangkan style default dari button internal */
all: unset;
display: contents; /* Agar slot bisa mengisi ruang */
}
</style>
</head>
<body>
<h1>Tombol Kustom dengan CSS Custom State</h1>
<my-button id="myBtn">Kirim Data</my-button>
<p>Klik tombol untuk melihat perubahan state.</p>
<script src="my-button.js"></script>
<script>
const myBtn = document.getElementById('myBtn');
myBtn.addEventListener('click', () => {
console.log('Button clicked!');
myBtn.startProcess();
});
</script>
</body>
</html>
// my-button.js
class MyButton extends HTMLElement {
// static formAssociated = true; // Tidak wajib untuk :state(), tapi relevan jika komponen ini bagian dari form
constructor() {
super();
// 1. Inisialisasi ElementInternals
this.internals = this.attachInternals();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
/* Styling internal Shadow DOM */
button {
font: inherit; /* Warisi font dari host */
color: inherit; /* Warisi warna teks dari host */
background: transparent; /* Background diambil dari host */
border: none;
padding: 0;
cursor: inherit;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
</style>
<button><slot></slot></button>
`;
}
// Metode untuk memulai proses (mengubah state menjadi 'loading')
startProcess() {
if (this.internals.states.has('loading')) return; // Hindari double-click
this.internals.states.add('loading'); // ✅ Tambahkan state 'loading'
this.internals.states.delete('success');
this.internals.states.delete('error');
this.shadowRoot.querySelector('button').disabled = true;
// Simulasikan proses asynchronous
const randomOutcome = Math.random();
setTimeout(() => {
if (randomOutcome > 0.6) {
this.finishProcess('success');
} else if (randomOutcome > 0.3) {
this.finishProcess('error');
} else {
this.finishProcess('idle'); // Kembali ke keadaan normal
}
}, 2000);
}
// Metode untuk mengakhiri proses (menghapus state 'loading' dan menambahkan state akhir)
finishProcess(finalState) {
this.internals.states.delete('loading'); // ✅ Hapus state 'loading'
this.shadowRoot.querySelector('button').disabled = false;
if (finalState === 'success') {
this.internals.states.add('success');
setTimeout(() => this.internals.states.delete('success'), 1500); // Hapus setelah beberapa waktu
} else if (finalState === 'error') {
this.internals.states.add('error');
setTimeout(() => this.internals.states.delete('error'), 1500);
}
// Jika 'idle', tidak perlu menambah state lain
}
}
customElements.define('my-button', MyButton);
Penjelasan Kode:
- HTML (
index.html): Kita mendefinisikan elemen<my-button>dan men-style-nya langsung di CSS global. Perhatikan bagaimana kita menggunakanmy-button:state(loading)untuk menargetkan status loading. - JavaScript (
my-button.js):- Di
constructor(), kita memanggilthis.attachInternals()dan menyimpannya dithis.internals. Ini adalah kunci untuk mengakses APIElementInternals, termasukstates. - Saat
startProcess()dipanggil, kita menggunakanthis.internals.states.add('loading')untuk memberi tahu browser bahwa komponen ini sekarang dalam status “loading”. Secara otomatis, CSS yang menargetkan:state(loading)akan diterapkan. - Setelah proses selesai,
finishProcess()menggunakanthis.internals.states.delete('loading')untuk menghapus status tersebut, dan menambahkansuccessatauerrorjika diperlukan.
- Di
Dengan pendekatan ini, JavaScript komponen hanya bertanggung jawab untuk mengelola status logisnya, dan CSS bertanggung jawab penuh atas presentasi visual dari status tersebut. Tidak ada lagi manipulasi atribut yang “kotor”!
5. Keunggulan CSS Custom State ✅
Mengadopsi CSS Custom State membawa beberapa keuntungan signifikan:
- Semantik dan Bersih: 🎯 State dielola secara internal oleh komponen sebagai bagian dari API-nya, bukan sebagai atribut DOM yang bersifat ad-hoc. Ini membuat kode lebih mudah dipahami dan lebih semantik.
- Performa Optimal: ⚡ Tidak perlu manipulasi atribut atau class di JavaScript untuk setiap perubahan state styling. Browser mengelola aplikasi pseudo-class secara native, yang dapat mengurangi layout thrashing dan meningkatkan performa rendering.
- Fleksibilitas Styling: 🎨 Developer aplikasi yang menggunakan Web Component Anda dapat men-style komponen dengan lebih presisi berdasarkan state kustom yang Anda definisikan, bahkan dari luar Shadow DOM. Ini meningkatkan customizability komponen Anda.
- Keterbacaan Kode dan Pemisahan Concern: 🧑💻 Memisahkan logika JavaScript (mengelola state) dari presentasi CSS (bereaksi terhadap state). Ini selaras dengan prinsip-prinsip clean code dan single responsibility.
- Aksesibilitas yang Lebih Baik: Meskipun
:state()itu sendiri bukan atribut ARIA, ia dapat digunakan untuk memicu perubahan visual yang mendukung aksesibilitas. Dengan state yang diekspos secara native, developer dapat memastikan bahwa komponen berperilaku dan terlihat konsisten di berbagai status. - Sinergi dengan Form-Associated Custom Elements: Karena
:state()adalah bagian dariElementInternals, ia berintegrasi dengan baik dengan custom element yang ingin berpartisipasi dalam form HTML, seperti:state(valid)atau:state(invalid).
6. Pertimbangan dan Batasan 🧐
Meskipun powerful, ada beberapa hal yang perlu Anda pertimbangkan:
- Dukungan Browser: 🌐 Per April 2024, CSS Custom State (
:state()) didukung secara default di browser berbasis Chromium (Chrome, Edge, Opera, Brave). Dukungan di Firefox dan Safari masih dalam pengembangan atau dibalik flag eksperimental. Penting untuk memeriksa Can I use untuk status terbaru dan mempertimbangkan progressive enhancement atau polyfill jika perlu untuk browser yang belum mendukung. - Fokus pada State Presentasi: 💡
:state()dirancang untuk mengekspos state yang relevan untuk styling. Ini bukan pengganti untuk state data yang perlu diakses atau dimanipulasi oleh JavaScript logika bisnis lainnya. - Nama State: Pilih nama state yang deskriptif dan hindari nama yang bisa bentrok dengan pseudo-class CSS bawaan browser (misalnya, jangan gunakan
:state(hover)). - Tidak untuk Semua Kasus: Untuk perubahan styling yang sangat sederhana atau tidak melibatkan state internal yang kompleks, atribut atau class mungkin masih lebih cepat untuk diimplementasikan di awal. Namun, untuk komponen yang lebih kaya interaksi,
:state()adalah pilihan yang lebih bersih dan skalabel.
Kesimpulan 🚀
CSS Custom State (:state()) adalah tambahan yang luar biasa untuk ekosistem Web Components, memungkinkan kita membangun komponen UI yang lebih kuat, lebih bersih, dan lebih mudah dikelola. Dengan memisahkan logika state dari manipulasi DOM untuk styling, kita dapat menciptakan pengalaman developer yang lebih baik dan aplikasi yang lebih berkinerja.
Meskipun dukungan browser masih berkembang, :state() adalah fitur yang patut Anda pelajari dan mulai eksplorasi, terutama jika Anda serius dalam membangun Design System atau library komponen yang berbasis Web Components. Ini adalah langkah maju menuju Web Components yang benar-benar mandiri dan fleksibel. Selamat mencoba!
🔗 Baca Juga
- Web Components: Membangun Komponen UI yang Reusable dan Framework-Agnostic
- Shadow DOM: Mengisolasi Style dan Markup di Web Components untuk UI yang Konsisten dan Bebas Konflik
- Membangun Custom Form Elements yang Robust dan Aksesibel dengan Web Components
- Menguasai Lifecycle Custom Elements Vanilla: Membangun Web Components yang Robust dan Prediktif