Studi Kasus Dark Mode: Menjaga Kontras dan Mencegah Tema Berkedip
Membedah implementasi tema NgodingSantai dari token warna, script awal, localStorage, ikon kontrol, hingga audit komponen terang dan gelap.
Dark mode NgodingSantai semula tampak seperti satu tombol yang mengganti warna. Masalah sebenarnya lebih luas: tema harus diterapkan sebelum stylesheet selesai dirender agar tidak berkedip, semua komponen harus memakai token yang sama, ikon harus menjelaskan aksi berikutnya, dan pilihan tetap bertahan ketika penyimpanan browser tersedia.
- Dua pilihan eksplisit: terang dan gelap.
- Preferensi sistem menjadi nilai awal ketika pengguna belum memilih.
localStoragedibungkustry/catchkarena penyimpanan dapat dibatasi.- Kontrol berupa button nyata dengan label yang berubah.
Gunakan token semantik, bukan warna per komponen
Stylesheet mendefinisikan warna berdasarkan fungsi: latar halaman, surface, teks utama, teks sekunder, garis, aksen, status, dan bayangan. Komponen tidak perlu mengetahui hex tema. Hal ini memperbaiki bug lama ketika tombol atau tab kategori memiliki background gelap tetapi teksnya tetap memakai warna muted terang.
:root {
--bg: #f8f9ff;
--surface: #fff;
--text: #182033;
--muted: #5d6679;
--line: #dfe3f0;
--accent: #5b4df5;
}
[data-theme="dark"] {
--bg: #0d1020;
--surface: #15192b;
--text: #f3f4ff;
--muted: #aab2ca;
--line: #2d3552;
--accent: #8b80ff;
color-scheme: dark;
}
Terapkan tema di head
theme.js dimuat sebelum stylesheet utama dan sebelum body. Script membaca pilihan tersimpan; jika tidak ada, ia membaca prefers-color-scheme. Atribut data-theme pada elemen HTML ditetapkan sedini mungkin sehingga browser tidak sempat menggambar tema terang lalu beralih gelap.
const stored = localStorage.getItem("ns-theme");
const preferred = matchMedia("(prefers-color-scheme: dark)").matches
? "dark" : "light";
document.documentElement.dataset.theme = stored || preferred;
Dalam source produksi, akses storage dibungkus penanganan error. Contoh ringkas di atas menunjukkan urutan keputusan, bukan seluruh proteksi.
Label tombol menjelaskan aksi
Jika tema saat ini gelap, label yang berguna adalah “Aktifkan mode terang”, bukan hanya “Tema” atau ikon bulan tanpa makna. JavaScript mengganti SVG dan aria-label setelah state berubah. Icon mempunyai aria-hidden karena nama aksesibel berasal dari tombol.
Jaga tombol primer tetap terbaca
Kelas umum .btn memakai surface dan text. Kelas .btn-primary kemudian menimpa background serta menetapkan putih secara eksplisit. Urutan ini mencegah aturan umum yang muncul belakangan mengubah warna teks tombol primer pada dark mode.
.btn { background: var(--surface); color: var(--text); }
.btn-primary,
.btn-primary:visited {
background: linear-gradient(135deg, var(--action), var(--action-strong));
color: #fff !important;
}
Audit semua state, bukan satu screenshot
Komponen diperiksa pada keadaan default, hover, focus-visible, active, disabled, dan aria-current. Navbar kategori sebelumnya tampak aktif pada halaman Tentang karena background aturan aktif tidak selaras dengan status sebenarnya. Script navigasi kini menentukan bagian aktif dari pathname dan membangun menu yang sama pada seluruh halaman.
| Komponen | Light | Dark | State tambahan |
|---|---|---|---|
| Button primary | Teks putih | Teks putih | hover, focus, disabled |
| Button ghost | Surface terang | Surface gelap | visited tidak berubah |
| Dropdown | Surface dan shadow | Surface dan border | hover, keyboard |
| Form | Background situs | color-scheme dark | error, autofill, focus |
Respons terhadap perubahan sistem
Ketika pengguna sudah memilih manual, perubahan preferensi sistem tidak mengambil alih pilihannya. Bila belum ada pilihan tersimpan, situs boleh mengikuti sistem. Tombol tetap menyediakan jalan keluar. Kebijakan ini perlu ditetapkan eksplisit agar event media query tidak menimpa keputusan pengguna.
Uji kegagalan storage dan JavaScript
Dalam mode privat tertentu atau lingkungan file lokal, storage bisa melempar error. Situs tetap memakai tema berdasarkan sistem dan tombol bekerja untuk sesi aktif. Tanpa JavaScript, atribut awal light membuat halaman tetap terbaca; fitur tema diperlakukan sebagai enhancement.
Checklist regresi
- Tema dipilih sebelum body dirender dan tidak berkedip jelas.
- Label tombol menyebut aksi yang akan terjadi.
- Text, muted text, border, card, dialog, code, dan form terbaca di dua tema.
- Focus-visible tidak hilang pada surface atau aksen.
- Reload dan navigasi ke artikel mempertahankan pilihan.
- Storage error tidak menghentikan script navigasi lain.
Referensi primer
Uji kontras tanpa mengandalkan persepsi satu monitor
Token teks dan background diperiksa dengan alat contrast ratio pada pasangan yang benar-benar muncul. Muted text pada surface, link aksen di callout, badge, placeholder form, serta focus ring diuji terpisah. Warna yang tampak nyaman di monitor desktop bisa kehilangan kontras pada ponsel dengan brightness rendah.
Kontrol native mengikuti color-scheme
color-scheme: dark membantu browser menggambar form, scrollbar, dan kontrol native sesuai tema. Namun input tetap mempunyai background, border, dan text token eksplisit agar konsisten. Autofill serta placeholder diperiksa karena keduanya dapat memakai warna dari user-agent stylesheet.
Hindari state yang tersebar
Atribut pada elemen HTML adalah state render utama. Storage hanya menyimpan preferensi untuk kunjungan berikutnya; ikon dan label diturunkan dari atribut yang aktif. Dengan satu sumber state, tombol tidak dapat menampilkan matahari sementara halaman sebenarnya masih memakai tema terang.
Periksa pasangan token, bukan hanya tampilan umum
Buka pemeriksa kontras dan gunakan preset terang (#182033 pada #ffffff), kemudian preset gelap (#f3f4ff pada #15192b). Keduanya adalah contoh pasangan token situs ini. Periksa juga teks muted, tombol, hover, fokus, dan latar kartu; satu pasangan lulus tidak mewakili semua komponen.
Uji rumus dengan hitam pada putih: hasilnya 21:1. Dua warna sama menghasilkan 1:1. Rasio yang ditampilkan dibulatkan, tetapi penentuan lulus memakai nilai tanpa pembulatan. Alat hanya menghitung warna opak; latar transparan perlu dikompositkan dahulu terhadap warna di belakangnya.
Catat hasil kedua tema pada checklist. Jangan mengubah tanggal publikasi asli ketika memperbarui palet; gunakan tanggal pembaruan untuk perubahan isi yang bermakna.
