Cara Membuat CSS Dark Mode dengan prefers-color-scheme
2026-07-09 · 14 min read
Situs ini, hibranwar.com, punya background #111110. Gelap. Bukan karena aku ikut-ikutan tren dark mode. Tapi karena 3 perusahaan yang aku jalankan, mayoritas tim dan kliennya pakai HP Android dengan dark mode nyala default. Kalo situs aku muncul putih terang di tengah malam, itu bukan masalah estetik. Itu masalah fungsional.
Dan ternyata, data menunjukkan bahwa aku ga sendirian.
Lebih dari 40% pengguna mobile di Asia Tenggara, termasuk Indonesia, menggunakan dark mode sebagai setting default di perangkat mereka [1]. Bukan karena mereka secara sadar memilih. Tapi karena Android 10 ke atas menyalakannya secara otomatis di banyak device OEM. Samsung, Xiaomi, OPPO, mereka semua punya dark mode toggle yang sering aktif dari pabrik.
Nah, pertanyaannya: kalo hampir setengah pengunjung situs kamu datang dengan system preference "dark", apakah situs kamu merespons? Atau tetap putih terang dan menyilaukan?
prefers-color-scheme untuk membaca preferensi OS pengunjung dan menampilkan tema yang sesuai. Tanpa JavaScript. Tanpa toggle. Tanpa database. Murni CSS.
Kenapa ini bukan soal estetika
Kebanyakan diskusi tentang dark mode di Indonesia masih berkutar di estetika. "Lebih keren." "Lebih modern." "Lebih enak diliat." Itu semua valid, tapi bukan alasan engineering.
Alasan engineering untuk dark mode:
- Penghematan baterai. Layar AMOLED (yang dominan di mid-range Android Indonesia) hemat 30-60% daya saat menampilkan piksel gelap [2]. Buat pengguna yang charge HP-nya cuma sekali sehari, ini signifikan.
- Kenyamanan visual di kondisi cahaya rendah. Pengguna Indonesia rata-rata menghabiskan 8+ jam di layar per hari. Sebagian besar setelah jam kerja, dalam kondisi cahaya rendah.
- Konsistensi dengan environment OS. Kalo seluruh HP pengguna sudah dark, lalu situs kamu muncul putih terang, itu jarring. Mata perlu re-adjust. Pengalaman itu ga sadar bikin orang leave lebih cepat.
- Accessibility. Beberapa pengguna dengan kondisi visual sensitif terhadap cahaya membutuhkan dark mode bukan sebagai preferensi, tapi sebagai kebutuhan.
Jadi ini bukan soal keren atau ga. Ini soal apakah situs kamu menghormati environment pengguna.
Tiga pendekatan dark mode dibandingkan
Sebelum masuk ke implementasi, aku mau jelaskan tiga pendekatan utama yang ada. Karena banyak tutorial di luar sana mencampur ketiganya tanpa menjelaskan trade-off.
| Pendekatan | Mekanisme | Butuh JS? | Respect OS? | User Override? | Kompleksitas |
|---|---|---|---|---|---|
| CSS-only (media query) | prefers-color-scheme |
Tidak | Ya | Tidak | Rendah |
| JS toggle (class-based) | body.dark-theme + localStorage |
Ya | Opsional | Ya | Sedang |
| Hybrid (CSS + JS) | Media query default + JS override | Minimal | Ya | Ya | Tinggi |
Pendekatan mana yang paling bagus? Tergantung kebutuhan. Tapi aku mau kasih opini praktisi: kalo kamu ga punya alasan kuat untuk kasih user toggle, CSS-only sudah lebih dari cukup.
Kenapa? Karena sebagian besar pengguna sudah mengatur preferensi mereka di level OS. Mereka ga mau ngatur ulang di setiap situs. Mereka cuma mau situs kamu mendengarkan apa yang sudah mereka pilih.
Implementasi: CSS custom properties + media query
Ini fondasi dari semua dark mode implementation yang bener. Custom properties (CSS variables) di :root, lalu override di dalam media query.
Langkah 1: definisikan design tokens di :root
/* Design tokens: light mode default */
:root {
--color-bg: #ffffff;
--color-surface: #f5f5f0;
--color-text: #1a1a18;
--color-text-muted: #6b6b63;
--color-accent: #8b6914;
--color-accent-alt: #3d6b45;
--color-border: #e0ddd6;
--color-code-bg: #f0ede6;
}
/* Dark mode override */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111110;
--color-surface: #191918;
--color-text: #ede9e3;
--color-text-muted: #8a8478;
--color-accent: #c8a882;
--color-accent-alt: #6b8f71;
--color-border: #2e2e2c;
--color-code-bg: #1e1e1c;
}
}Catatan: warna dark mode di atas itu persis yang aku pakai di hibranwar.com. Bukan random. Setiap token punya ratio kontras yang sudah dihitung biar memenuhi WCAG AA minimum (4.5:1 untuk teks, 3:1 untuk elemen dekoratif).
Langkah 2: gunakan variables di seluruh stylesheet
body {
background-color: var(--color-bg);
color: var(--color-text);
transition: background-color 0.3s ease, color 0.3s ease;
}
a {
color: var(--color-accent);
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
}
code {
background: var(--color-code-bg);
color: var(--color-accent);
}
.text-muted {
color: var(--color-text-muted);
}Ini aja. Ga perlu duplikasi selector. Ga perlu file CSS terpisah untuk dark mode. Variables berubah, seluruh situs berubah.
Langkah 3: tambahkan color-scheme meta tag
<!-- Di <head>, sebelum stylesheet -->
<meta name="color-scheme" content="light dark">Meta tag ini memberitahu browser bahwa situs kamu mendukung kedua mode. Browser akan langsung merender scrollbar, form elements, dan UI bawaan dalam warna yang sesuai. Tanpa meta tag ini, browser mungkin tetap merender elemen bawaan dalam light mode meskipun CSS kamu sudah dark.
Kamu juga bisa set ini di CSS:
:root {
color-scheme: light dark;
}Tapi meta tag di HTML lebih baik karena diparsing sebelum CSS di-load. Ini menghindari flash of incorrect theme, terutama di koneksi lambat yang umum di Indonesia.
Dark-first vs light-first: mana yang dipilih?
Hampir semua tutorial di internet mengasumsikan light-first. Define light di :root, override ke dark di media query. Ini approach yang aku tunjukkan di atas.
Tapi ada approach lain: dark-first. Define dark di :root, override ke light di media query.
/* Dark-first approach */
:root {
--color-bg: #111110;
--color-text: #ede9e3;
/* ... dark values sebagai default */
}
@media (prefers-color-scheme: light) {
:root {
--color-bg: #ffffff;
--color-text: #1a1a18;
/* ... light values sebagai override */
}
}Kapan pakai dark-first?
- Kalo mayoritas audiens kamu pakai dark mode (seperti developer, gamers, pengguna Android di Indonesia)
- Kalo brand identity kamu memang gelap (seperti hibranwar.com)
- Kalo kamu mau fallback ke dark untuk browser lama yang ga support
prefers-color-scheme
hibranwar.com pakai dark-first. Karena kalo browser ga ngerti media query ini, yang muncul tetap dark theme yang merupakan intended experience. Bukan white page yang menyilaukan.
Ini keputusan arsitektur, bukan estetika. Sama seperti pilihan antara website statis vs WordPress, ini soal memilih default yang benar untuk audiens yang benar.
Menambahkan user toggle (kalo memang perlu)
Ada kasus di mana kamu perlu kasih user pilihan manual. Misalnya situs dokumentasi teknis yang dipake di berbagai kondisi cahaya, atau situs e-commerce yang punya light branding tapi audiens dark mode-nya signifikan.
Ini cara yang bersih, tanpa framework, tanpa dependency:
HTML
<button id="theme-toggle"
aria-label="Toggle dark mode"
type="button">
<span class="icon-sun">☼</span>
<span class="icon-moon">☾</span>
</button>CSS: data attribute sebagai override
/* Default: respect system preference */
:root {
--color-bg: #ffffff;
--color-text: #1a1a18;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111110;
--color-text: #ede9e3;
}
}
/* Manual override via data attribute */
[data-theme="dark"] {
--color-bg: #111110;
--color-text: #ede9e3;
}
[data-theme="light"] {
--color-bg: #ffffff;
--color-text: #1a1a18;
}JavaScript: simpan pilihan, tanpa flash
// Inline di <head> supaya jalan sebelum render
(function() {
var saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
}
})();
// Toggle handler (di footer atau DOMContentLoaded)
document.getElementById('theme-toggle')
.addEventListener('click', function() {
var current = document.documentElement
.getAttribute('data-theme');
var next;
if (current === 'dark') {
next = 'light';
} else if (current === 'light') {
next = 'dark';
} else {
// No override yet. Check system preference.
var prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
next = prefersDark ? 'light' : 'dark';
}
document.documentElement
.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});Poin kritis: script pertama (yang membaca localStorage) harus inline di <head>. Bukan di file external. Bukan di DOMContentLoaded. Kalo di-load belakangan, pengguna akan lihat flash of wrong theme (FOWT) selama beberapa ratus milidetik. Di koneksi 3G Indonesia, "beberapa ratus milidetik" bisa jadi 1-2 detik full flash putih. Ga acceptable.
Gambar dan media di dark mode
Yang sering dilupakan: dark mode bukan cuma soal background dan teks. Gambar, ikon, dan media juga perlu di-handle.
Gambar dengan background putih
Logo atau diagram dengan background putih akan terlihat "meledak" di dark mode. Solusinya:
/* Kurangi brightness gambar di dark mode */
@media (prefers-color-scheme: dark) {
img:not([src*=".svg"]) {
filter: brightness(0.9) contrast(1.05);
}
/* Invert diagram hitam-putih */
.diagram-bw {
filter: invert(1) hue-rotate(180deg);
}
}
/* Atau gunakan <picture> untuk gambar berbeda */
/* <picture>
<source srcset="hero-dark.webp"
media="(prefers-color-scheme: dark)">
<img src="hero-light.webp" alt="...">
</picture> */Element <picture> dengan media attribute itu solusi paling bersih. Tapi realistis, kalo kamu punya ratusan gambar, ga mungkin bikin versi dark untuk semuanya. Filter CSS itu pragmatic compromise yang cukup.
SVG inline
SVG inline bisa langsung pake CSS variables. Ini yang paling elegan:
<svg viewBox="0 0 24 24">
<path fill="var(--color-text)"
d="M12 2L2 7l10 5 10-5-10-5z"/>
</svg>Warna SVG otomatis mengikuti theme. Zero JavaScript. Zero class toggling.
Performance impact: dark mode tanpa trade-off
Salah satu kekhawatiran yang sering aku dengar: "Kalo nambah dark mode, CSS jadi besar dong?"
Tergantung caranya.
Kalo kamu duplikasi semua selector di dalam media query, ya besar. Kalo kamu pakai CSS custom properties, yang berubah cuma values-nya. Satu blok :root override itu biasanya 200-500 bytes. Itu ga ada artinya dibanding satu gambar hero yang 200KB.
Perbandingan overhead di situs nyata:
| Metode | Tambahan CSS | Tambahan JS | Render Blocking? |
|---|---|---|---|
| CSS custom properties + media query | ~300 bytes | 0 | Tidak |
| Separate dark.css file | ~2-5 KB | 0 | Ya (extra request) |
| JS toggle + class override | ~1-3 KB | ~500 bytes | Minimal (inline) |
| Full framework (styled-components, etc.) | Varies (10-50 KB runtime) | 10-50 KB | Ya |
Angka di baris pertama itu bukan teori. Itu kenyataan di hibranwar.com. Situs ini punya dark theme native dan total CSS-nya tetap satu file style.css yang ringan. Ga ada JavaScript untuk theming. Nol. Sesuai prinsip yang sudah aku bahas soal kenapa page speed itu bukan cuma soal ranking tapi soal visibilitas AI.
Kesalahan umum (dan cara menghindarinya)
Setelah membangun dan me-review puluhan situs, ini kesalahan yang paling sering aku temui:
1. Hardcode warna di seluruh CSS
Kalo kamu punya color: #333 tersebar di 200 selector, dark mode jadi nightmare. Solusi: refactor ke custom properties dulu. Ini investasi satu kali yang bayar balik selamanya.
2. Lupa form elements
<input>, <select>, <textarea> punya styling default dari browser. Di dark mode, form field putih terang di tengah halaman gelap itu sangat mengganggu. Selalu style form elements secara eksplisit:
input, select, textarea {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}3. Kontras terlalu rendah
Dark mode bukan berarti abu-abu di atas hitam. Teks utama harus punya kontras ratio minimal 4.5:1. Aku pakai #ede9e3 di atas #111110, yang memberikan ratio 13.8:1. Jauh di atas minimum.
4. Pure black (#000000) background
Pure black di layar AMOLED memang hemat baterai maksimal. Tapi pure black juga bikin teks "mengambang" dan sulit dibaca lama. Off-black (#111110, #1a1a1a) jauh lebih nyaman. Apple dan Google Material Design keduanya merekomendasikan off-black, bukan pure black [3].
5. Ga test di device asli
Browser DevTools punya toggle untuk prefers-color-scheme. Tapi itu ga menggantikan testing di HP Android murah dengan layar TN yang kontrasnya rendah. Situs kamu harus tetap readable di Redmi Note seharga 2 juta, bukan cuma di MacBook Pro.
Fungsi CSS baru: light-dark()
Ini bonus untuk yang suka bleeding edge. CSS sekarang punya fungsi light-dark() yang memungkinkan kamu define kedua nilai dalam satu deklarasi [4]:
:root {
color-scheme: light dark;
}
body {
background-color: light-dark(#ffffff, #111110);
color: light-dark(#1a1a18, #ede9e3);
}
a {
color: light-dark(#8b6914, #c8a882);
}Nilai pertama untuk light, kedua untuk dark. Clean. Compact. Tapi per April 2026, browser support-nya sudah ada di Chrome 123+, Firefox 120+, dan Safari 17.5+. Cukup bagus untuk progressive enhancement, tapi masih perlu fallback kalo kamu target browser lama.
Aku belum pake ini di hibranwar.com karena approach aku terhadap web fonts juga konservatif. Aku prefer teknik yang sudah proven di semua browser yang realistically dipakai audiens Indonesia, bukan yang paling baru.
Schema markup untuk dark mode?
Pertanyaan yang mungkin muncul: apakah ada structured data untuk dark mode? Jawaban singkat: ga ada schema.org property khusus untuk dark mode.
Tapi ada hal terkait yang perlu diperhatikan. Kalo situs kamu punya og:image untuk social sharing, gambar itu ga berubah berdasarkan dark/light mode. Pastikan OG image kamu readable di kedua konteks. Text putih di background gelap itu aman untuk kedua mode. Text hitam di background putih akan look off di dark mode social feeds.
Dan kalo kamu pakai color-scheme: light dark di meta tag, Google memahami ini sebagai sinyal bahwa situs kamu responsive terhadap user preferences. Ini bukan ranking factor langsung, tapi kontributor ke overall page experience signal.
Frequently Asked Questions
Apakah dark mode mempengaruhi SEO atau ranking Google?
Tidak secara langsung. Google tidak memberikan ranking boost untuk situs yang punya dark mode. Tapi dark mode yang diimplementasikan dengan CSS custom properties (bukan JavaScript rendering) berarti situs kamu tetap ringan dan accessible. Dan itu berkontribusi ke Core Web Vitals dan page experience, yang memang faktor ranking. Jadi pengaruhnya indirect tapi real.
Haruskah aku bikin dark mode toggle atau cukup ikut system preference?
Untuk kebanyakan situs, ikut system preference sudah cukup. Toggle hanya diperlukan kalo audiens kamu punya alasan kuat untuk override (misalnya developer yang sering switch antara reading mode dan coding mode). Kalo kamu ga yakin, mulai tanpa toggle. Tambahkan nanti kalo ada permintaan.
Browser mana yang support prefers-color-scheme?
Per 2026, semua browser modern: Chrome 76+, Firefox 67+, Safari 12.1+, Edge 79+, Samsung Internet 10+, Opera 62+. Ini berarti 97%+ pengguna global sudah ter-cover. Browser yang ga support akan melihat default theme kamu, yang seharusnya sudah usable tanpa dark mode.
Apa bedanya color-scheme property dan prefers-color-scheme media query?
color-scheme memberitahu browser bahwa halaman kamu mendukung mode tertentu, sehingga browser bisa menyesuaikan UI bawaan (scrollbar, form controls, dll). prefers-color-scheme adalah media query yang membaca preferensi pengguna dari OS dan memungkinkan kamu menulis CSS yang berbeda. Keduanya harus dipakai bersama untuk implementasi yang lengkap.
Gimana cara test dark mode tanpa mengubah setting OS?
Di Chrome DevTools: buka Command Palette (Ctrl+Shift+P), ketik "Emulate CSS prefers-color-scheme", pilih dark atau light. Di Firefox: ketik about:config, cari ui.systemUsesDarkTheme, set ke 1 (dark) atau 0 (light). Tapi tetap test di device asli sebelum deploy. DevTools ga bisa simulasikan layar TN panel murah.
References
- Android Developers. "Dark theme." Android Developers Documentation, 2024. Link
- Purdue University. "Dark Mode vs. Light Mode: Which Is Better?" Purdue OLED Research, 2021. Link
- Google. "Dark theme." Material Design Guidelines, 2024. Link
- MDN Web Docs. "light-dark()." Mozilla Developer Network, 2024. Link
- web.dev. "prefers-color-scheme: Hello darkness, my old friend." Google web.dev, 2024. Link