Preferensi sistem dari prefers-color-scheme otomatis, tetapi tidak selalu cukup: pengguna bisa saja memakai sistem terang sambil menginginkan halaman tertentu dalam mode gelap, atau sebaliknya. Theme switching yang utuh adalah gabungan semua yang telah kita bangun di section ini — token semantik sebagai sumber nilai, atribut data-tema sebagai sakelar, penimpaan token per tema, media query sebagai default, transisi agar perubahan tidak melompat, dan localStorage agar pilihan diingat di kunjungan berikutnya. Chapter ini merangkainya menjadi satu pola kerja yang ringkas.

Pola: Atribut Sebagai Sakelar, Token Sebagai Perubahan #

Fondasinya satu selektor: [data-tema="gelap"] menimpa token pada elemen pembawanya. Karena custom properties diwarisi, menempelkan atribut pada <html> mengubah seluruh halaman; menempelkannya pada satu panel hanya mengubah panel itu — fleksibilitas yang kita lihat pada Chapter Custom Properties dan Theming :

:root {
  --latar: rgb(255, 255, 255);
  --teks: rgb(15, 23, 42);
  --aksen: rgb(29, 78, 216);
  --garis: rgb(226, 232, 240);
}

[data-tema="terang"] {
  --latar: rgb(255, 255, 255);
  --teks: rgb(15, 23, 42);
  --aksen: rgb(29, 78, 216);
  --garis: rgb(226, 232, 240);
}

[data-tema="gelap"] {
  --latar: rgb(2, 6, 23);
  --teks: rgb(226, 232, 240);
  --aksen: rgb(147, 197, 253);
  --garis: rgb(30, 41, 59);
}

body,
.kartu {
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.kartu {
  background-color: var(--latar);
  color: var(--teks);
  border: 1px solid var(--garis);
  border-radius: 12px;
  padding: 16px;
}

Mari kita telaah keputusannya:

  1. Tiga blok token: :root sebagai default, [data-tema="terang"] untuk mengunci kembali secara eksplisit, dan [data-tema="gelap"] untuk tema gelap. Blok terang yang eksplisit membuat panel terang tetap terang walau leluhurnya gelap.
  2. Transisi pada properti visual: transition menyasar background-color, color, dan border-color — bukan custom properties-nya. Nilai computed akhir tidak berubah oleh transisi, sehingga probe tetap valid.
  3. Satu aturan .kartu untuk semua tema: kartu tidak tahu tema apa pun; ia hanya membaca token. Perubahan tema tidak pernah menyentuh aturan komponen.

Skrip pendampingnya kecil — membalik atribut pada <html> dan menyimpan pilihannya:

html index.html
<button id="sakelar" type="button">Ganti Tema</button>

<script>
  const KUNCI = "tema-pilihan";
  const akar = document.documentElement;

  document.getElementById("sakelar").addEventListener("click", () => {
    const berikut = akar.getAttribute("data-tema") === "gelap" ? "terang" : "gelap";
    akar.setAttribute("data-tema", berikut);
    localStorage.setItem(KUNCI, berikut);
  });

  const tersimpan = localStorage.getItem(KUNCI);
  if (tersimpan === "terang" || tersimpan === "gelap") {
    akar.setAttribute("data-tema", tersimpan);
  }
</script>

Mari kita telaah skripnya:

  1. Klik tombol membalik atribut: skrip membaca data-tema saat ini, memilih lawannya, lalu menetapkannya pada document.documentElement — satu penulisan atribut menggerakkan seluruh halaman.
  2. Pilihan disimpan: localStorage.setItem(KUNCI, berikut) menuliskan pilihan pada penyimpanan per-origin browser, sehingga kunjungan berikutnya dapat membacanya kembali.
  3. Pilihan dipulihkan saat dimuat: baris terakhir membaca kembali nilai tersimpan dan menerapkannya; validasi sederhana mencegah nilai asing menempel sebagai atribut.

Pola ini juga menyisakan tempat yang tepat untuk preferensi sistem. Media query dari Chapter Prefers-color-scheme dapat menjadi default ketika pengguna belum memilih apa pun, dengan memanfaatkan selector negasi:

css
@media (prefers-color-scheme: dark) {
  :root:not([data-tema]) {
    --latar: rgb(2, 6, 23);
    --teks: rgb(226, 232, 240);
    --aksen: rgb(147, 197, 253);
    --garis: rgb(30, 41, 59);
  }
}

Karena :root:not([data-tema]) hanya cocok selama atribut belum ada, pilihan eksplisit pengguna — yang selalu menempel atribut — otomatis memenangkan preferensi manual atas preferensi sistem. Urutan prioritasnya menjadi tegas: pilihan tersimpan di localStorage menang, preferensi sistem menjadi bawaan, dan tema terang menjadi jaring pengaman terakhir.

Demo: Panel Terang, Panel Gelap, dan Sakelar #

Demo chapter ini memasang ketiga blok token beserta transisi dan skrip di atas. Dua panel membawa atribut eksplisit sehingga keduanya dapat diverifikasi otomatis, sedangkan tombolnya mengubah tema <html> sesuai cerita pengguna nyata. Hasil di Browser:

text
Panel terang tampil berlatar rgb(255, 255, 255) dengan teks
rgb(15, 23, 42) dari blok [data-tema="terang"].
Panel gelap tampil berlatar rgb(2, 6, 23) dengan teks rgb(226, 232, 240)
dan aksen rgb(147, 197, 253) dari blok [data-tema="gelap"].

Kelima nilai tersebut diverifikasi otomatis melalui penanda data-expected. Klik tombol Ganti Tema pada demo untuk mengubah seluruh halaman — atribut pada <html> berganti, transisi membuat perpindahannya halus, dan pilihan tersimpan di localStorage. Untuk menganimasikan perubahan custom properties itu sendiri, @property (Baseline 2024) mendaftarkan token sebagai variabel bertipe yang dapat ditransisi; dan tema dapat pula merespons ukuran kontainer lewat Chapter Container Style Queries Overview .

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh-sakelar-tema.css

Unduh berkas
css
:root {
  --latar: rgb(255, 255, 255);
  --teks: rgb(15, 23, 42);
  --aksen: rgb(29, 78, 216);
  --garis: rgb(226, 232, 240);
}

[data-tema="terang"] {
  --latar: rgb(255, 255, 255);
  --teks: rgb(15, 23, 42);
  --aksen: rgb(29, 78, 216);
  --garis: rgb(226, 232, 240);
}

[data-tema="gelap"] {
  --latar: rgb(2, 6, 23);
  --teks: rgb(226, 232, 240);
  --aksen: rgb(147, 197, 253);
  --garis: rgb(30, 41, 59);
}

body,
.kartu {
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

body {
  background-color: var(--latar);
  color: var(--teks);
}

.kartu {
  background-color: var(--latar);
  color: var(--teks);
  border: 1px solid var(--garis);
  border-radius: 12px;
  padding: 16px;
}

.aksen { color: var(--aksen); font-weight: 600; }