Kembali ke Blog

Toggle Tema 3 Arah: System, Light, Dark — Bukan Cuma prefers-color-scheme

#css#ux#tailwind
Toggle Tema 3 Arah: System, Light, Dark — Bukan Cuma prefers-color-scheme

Situs ini punya tombol tema di navbar yang, kalau diklik, siklus lewat tiga state: system → light → dark → balik ke system lagi. Awalnya saya kira cukup ngikutin prefers-color-scheme doang tanpa kontrol manual apapun — kan itu udah “benar” secara teori, ngikutin preferensi OS. Tapi ternyata ada alasan kuat buat nambahin toggle eksplisit, dan alasannya baru kelihatan setelah saya mikirin siapa aja yang buka situs ini.

Kenapa prefers-color-scheme doang nggak cukup

Beberapa pengunjung browser/OS-nya di-set light secara default — laptop kantor yang policy IT-nya maksa light mode, misalnya — tapi mereka pribadi lebih nyaman baca teks panjang dalam gelap, atau sebaliknya. Static site nggak tahu preferensi personal kayak gitu kalau cuma ngandelin signal OS. Saya sendiri kejadian versi kebalikannya: laptop pribadi saya defaultnya dark, tapi siang hari di ruangan yang terang, saya justru lebih nyaman baca di light mode. Kalau situs ini cuma ngikutin OS mentah-mentah, saya sendiri bakal kesel tiap kali buka situs saya sendiri di siang hari.

Implementasi lewat custom variant Tailwind

Alih-alih pakai dark: bawaan Tailwind yang defaultnya ngikutin media query langsung, saya override lewat @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); di global.css. Jadi kelas dark: di seluruh komponen ngikutin atribut data-theme di elemen <html>, bukan raw media query lagi.

Logikanya sendiri ada di client-side script (ThemeToggle.astro). Ada tiga mode — system, light, dark. Kalau mode-nya system, resolved theme-nya ngikutin window.matchMedia('(prefers-color-scheme: dark)'), jadi tetep ngikutin OS secara default. Begitu user klik toggle, mode-nya disimpen ke localStorage dan atribut data-theme di-set langsung, override signal OS-nya.

Detail kecil yang ternyata penting

Semua akses localStorage dan matchMedia saya bungkus try/catch, dan ini bukan defensive coding tanpa alasan — saya ketemu masalahnya beneran. Waktu testing di private browsing mode Safari di iOS, localStorage.setItem bisa throw QuotaExceededError walau kuota sebenarnya masih penuh, karena Safari sengaja block write ke localStorage di mode private tertentu. Tanpa try/catch, toggle-nya bakal ngelempar unhandled exception dan bikin script-nya berhenti di tengah jalan, yang efeknya lebih parah dari sekadar preferensi nggak ke-save — seluruh halaman bisa gagal render tema dengan benar. Jadi fallback-nya sekarang diem-diem aja ke default kalau API itu nggak bisa diakses, situs tetep render dengan benar walau preferensinya nggak ke-persist antar sesi.

Saya juga pasang listener buat perubahan OS runtime — kalau mode masih system dan user ganti preferensi OS-nya di tengah sesi browsing (bukan reload halaman), tema situsnya ikut update otomatis tanpa refresh. Ini detail kecil yang mungkin nggak ada satu pun pengunjung yang bakal notice secara sadar, tapi kalau nggak ada, akan terasa “rusak” dengan cara yang susah dijelasin — tema yang nggak update padahal preferensi OS-nya udah beda.

Awalnya saya kira solusi paling sederhana adalah nggak punya toggle sama sekali, cuma ngandelin OS penuh. Ternyata versi yang lebih baik bukan “sederhana secara absolut”, tapi “ngasih kontrol ke user tanpa maksa mereka setting apapun” — default-nya tetep ngikutin OS, tapi override-nya tetep tersedia buat yang butuh, termasuk buat saya sendiri pas siang hari di ruangan terang.