CSS WEB-COMPONENTS FRONTEND STYLING WEB-STANDARDS UI-UX INTERACTIVITY JAVASCRIPT DEVELOPER-EXPERIENCE MODERN-WEB

CSS Custom State (`:state()`): Membangun Web Components yang Lebih Interaktif dan Terkontrol

⏱️ 15 menit baca
👨‍💻

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:

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?

  1. ElementInternals: Setiap custom element dapat mengakses objek ElementInternals melalui this.attachInternals() di konstruktornya.
  2. states Set: Objek ElementInternals memiliki properti states yang merupakan sebuah Set. Anda bisa menambahkan atau menghapus nama state kustom ke/dari Set ini.
  3. 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:

  1. HTML (index.html): Kita mendefinisikan elemen <my-button> dan men-style-nya langsung di CSS global. Perhatikan bagaimana kita menggunakan my-button:state(loading) untuk menargetkan status loading.
  2. JavaScript (my-button.js):
    • Di constructor(), kita memanggil this.attachInternals() dan menyimpannya di this.internals. Ini adalah kunci untuk mengakses API ElementInternals, termasuk states.
    • Saat startProcess() dipanggil, kita menggunakan this.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() menggunakan this.internals.states.delete('loading') untuk menghapus status tersebut, dan menambahkan success atau error jika diperlukan.

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:

6. Pertimbangan dan Batasan 🧐

Meskipun powerful, ada beberapa hal yang perlu Anda pertimbangkan:

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