Kenapa ini penting
Mode gelap sudah menjadi fitur standar: banyak orang memakai HP dalam mode gelap sepanjang hari, terutama malam hari. Aplikasi yang tetap putih menyilaukan terasa ketinggalan zaman. Selain itu, perusahaan biasanya punya design system dengan warna, font, dan jarak yang sudah ditetapkan; developer harus bisa menerjemahkannya menjadi token yang dipakai konsisten.
Tailwind v4 menangani keduanya dengan elegan: variant dark: untuk mode gelap, @custom-variant untuk mengatur kapan mode gelap aktif (otomatis dari sistem atau lewat tombol), dan @theme untuk design tokens.
Konsep inti
Variant dark:
Secara default, dark: mengikuti pengaturan sistem operasi pengguna lewat media query prefers-color-scheme: dark. Kamu cukup menambahkan pasangan gelap: bg-white dark:bg-gray-900, text-gray-900 dark:text-gray-100. Tidak perlu JavaScript sama sekali.
Mode manual dengan @custom-variant
Banyak aplikasi ingin memberi pengguna tombol pilihan tema. Untuk itu, ubah cara kerja dark: agar mengikuti class .dark di elemen <html>:
@custom-variant dark (&:where(.dark, .dark *));
Artinya: "variant dark aktif kalau elemen ini, atau leluhurnya, punya class dark". :where() dipakai agar specificity tetap nol, sehingga tidak mengacaukan urutan class. Kalau lebih suka atribut data: @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));.
Ini menggantikan darkMode: 'class' di tailwind.config.js versi v3.
Toggle tema dengan JavaScript
Pola yang umum dipakai ada tiga pilihan: terang, gelap, atau ikuti sistem. Simpan pilihan di localStorage, dan jalankan skrip kecil di dalam <head> sebelum halaman dirender, agar tidak terjadi "kedipan putih" (flash of wrong theme) saat halaman dimuat.
Analogi: skrip di <head> itu seperti menyalakan lampu kamar sebelum tamu masuk. Kalau baru dinyalakan setelah tamu masuk, mereka sempat melihat kamar gelap sesaat.
Design tokens di @theme
Design token adalah nilai desain bernama: warna brand, font, radius, jarak. Di v4, setiap variabel di @theme otomatis menjadi utility dan CSS variable. Namespace penting:
| Namespace | Menghasilkan |
|---|---|
--color-* |
bg-*, text-*, border-*, dll |
--font-* |
font-* |
--text-* |
text-* (ukuran font) |
--radius-* |
rounded-* |
--shadow-* |
shadow-* |
--breakpoint-* |
variant responsif |
--spacing |
skala spacing |
Token semantik untuk tema gelap
Daripada menulis bg-white dark:bg-gray-900 di ratusan tempat, buat token semantik (berdasarkan fungsi, bukan warna): --color-surface, --color-ink, --color-muted. Lalu timpa nilainya saat .dark aktif. Komponen cukup memakai bg-surface text-ink, dan otomatis berganti tema. Ini cara design system besar bekerja.
Contoh kode
/* src/style.css */
@import "tailwindcss";
/* 1. dark: mengikuti class .dark di <html>, bukan pengaturan sistem */
@custom-variant dark (&:where(.dark, .dark *));
/* 2. Design tokens */
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--radius-card: 1rem; /* → rounded-card */
/* Token semantik versi terang */
--color-surface: #ffffff;
--color-surface-alt: #f3f4f6;
--color-ink: #111827;
--color-muted: #6b7280;
--color-primary: oklch(0.55 0.2 265);
}
/* 3. Timpa token saat mode gelap */
@layer base {
.dark {
--color-surface: #0b1120;
--color-surface-alt: #1e293b;
--color-ink: #f1f5f9;
--color-muted: #94a3b8;
--color-primary: oklch(0.7 0.16 265);
}
}
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Jalankan SEBELUM render agar tidak ada kedipan tema salah -->
<script>
const simpan = localStorage.getItem("tema"); // "terang" | "gelap" | null
const sistemGelap = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle(
"dark",
simpan === "gelap" || (simpan === null && sistemGelap)
);
</script>
<script type="module" src="/src/main.js"></script>
</head>
<body class="min-h-dvh bg-surface-alt font-sans text-ink">
<header class="flex items-center justify-between bg-surface px-6 py-4">
<span class="font-bold">Catatanku</span>
<div class="flex gap-2 text-sm">
<button data-tema="terang" class="cursor-pointer rounded-md px-3 py-1.5 hover:bg-surface-alt">☀️ Terang</button>
<button data-tema="gelap" class="cursor-pointer rounded-md px-3 py-1.5 hover:bg-surface-alt">🌙 Gelap</button>
<button data-tema="sistem" class="cursor-pointer rounded-md px-3 py-1.5 hover:bg-surface-alt">💻 Sistem</button>
</div>
</header>
<main class="p-6">
<!-- Token semantik: otomatis berganti tema -->
<article class="max-w-md rounded-card bg-surface p-6 shadow-sm">
<h2 class="text-xl font-semibold">Belanja mingguan</h2>
<p class="mt-2 text-muted">Telur, beras, sayur bayam, kopi.</p>
<button class="mt-4 rounded-lg bg-primary px-4 py-2 text-white">Tandai selesai</button>
</article>
<!-- dark: variant langsung, untuk kasus khusus -->
<p class="mt-6 rounded-lg bg-yellow-100 p-3 text-yellow-900 dark:bg-yellow-500/15 dark:text-yellow-200">
Catatan penting yang warnanya diatur manual.
</p>
</main>
</body>
</html>
// src/main.js
import "./style.css";
function terapkanTema(pilihan) {
if (pilihan === "sistem") {
localStorage.removeItem("tema");
const gelap = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", gelap);
} else {
localStorage.setItem("tema", pilihan);
document.documentElement.classList.toggle("dark", pilihan === "gelap");
}
}
document.querySelectorAll("[data-tema]").forEach((tombol) => {
tombol.addEventListener("click", () => terapkanTema(tombol.dataset.tema));
});
// Ikuti perubahan pengaturan sistem kalau pengguna memilih "sistem"
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (localStorage.getItem("tema") === null) {
document.documentElement.classList.toggle("dark", e.matches);
}
});
// → klik "Gelap": <html class="dark">, kartu jadi gelap; reload halaman tetap gelap
Kesalahan umum
- Menulis
darkMode: 'class'ditailwind.config.js. Itu cara v3. Di v4 pakai@custom-variant dark (&:where(.dark, .dark *));. - Skrip tema di akhir
<body>, sehingga halaman sempat tampil terang lalu berkedip gelap. Taruh di<head>. - Menaruh class
darkdi elemen yang salah, misalnya hanya di satudiv. Untuk seluruh halaman, taruh di<html>. - Hanya membalik warna latar, lupa teks, border, dan bayangan. Gunakan token semantik agar semuanya konsisten.
- Memakai hitam murni
#000dan putih murni untuk mode gelap. Kontrasnya terlalu tajam; abu-abu sangat gelap kebiruan lebih nyaman.
Latihan
- Mudah, Buat kartu yang putih dengan teks gelap di mode terang, dan
gray-800dengan teks terang di mode gelap, memakaidark:(mode sistem default). - Sedang, Ubah proyek agar mode gelap dikontrol oleh atribut
data-theme="dark"di<html>, lalu buat satu tombol yang men-toggle atribut itu. - Menantang, Buat token semantik
--color-border-softyang bernilai#e5e7ebdi mode terang dan#334155di mode gelap, lalu pakai di daftardivide-y.
Lihat solusi
- Kartu mode sistem:
<div class="rounded-xl bg-white p-6 text-gray-900 dark:bg-gray-800 dark:text-gray-100">
Kartu yang mengikuti tema sistem operasi.
</div>
- Mode lewat atribut data:
@import "tailwindcss";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
<button id="toggle" class="rounded border px-3 py-1 dark:border-gray-600 dark:text-white">Ganti tema</button>
<script>
document.getElementById("toggle").addEventListener("click", () => {
const html = document.documentElement;
const gelap = html.dataset.theme === "dark";
html.dataset.theme = gelap ? "light" : "dark";
localStorage.setItem("tema", html.dataset.theme);
});
</script>
- Token border:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-border-soft: #e5e7eb;
}
@layer base {
.dark {
--color-border-soft: #334155;
}
}
<ul class="divide-y divide-border-soft rounded-lg border border-border-soft">
<li class="p-3">Satu</li>
<li class="p-3">Dua</li>
</ul>
Ringkasan
dark:default mengikutiprefers-color-schemesistem operasi.- Mode manual:
@custom-variant dark (&:where(.dark, .dark *));lalu toggle classdarkdi<html>. - Simpan pilihan di
localStoragedan jalankan skrip tema di<head>untuk mencegah kedipan. - Semua variabel
@thememenjadi utility dan CSS variable; namespace menentukan utility-nya. - Token semantik (
--color-surface,--color-ink) yang ditimpa di.darkmembuat tema konsisten.