kodingup.camp

Beranda/Tailwind Mastery

Kustomisasi Tema & Mode Gelap

Modul 12 dari 163 menit bacaTheme Customization & Dark Mode

Isi modul

    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

    1. Menulis darkMode: 'class' di tailwind.config.js. Itu cara v3. Di v4 pakai @custom-variant dark (&:where(.dark, .dark *));.
    2. Skrip tema di akhir <body>, sehingga halaman sempat tampil terang lalu berkedip gelap. Taruh di <head>.
    3. Menaruh class dark di elemen yang salah, misalnya hanya di satu div. Untuk seluruh halaman, taruh di <html>.
    4. Hanya membalik warna latar, lupa teks, border, dan bayangan. Gunakan token semantik agar semuanya konsisten.
    5. Memakai hitam murni #000 dan putih murni untuk mode gelap. Kontrasnya terlalu tajam; abu-abu sangat gelap kebiruan lebih nyaman.

    Latihan

    1. Mudah, Buat kartu yang putih dengan teks gelap di mode terang, dan gray-800 dengan teks terang di mode gelap, memakai dark: (mode sistem default).
    2. Sedang, Ubah proyek agar mode gelap dikontrol oleh atribut data-theme="dark" di <html>, lalu buat satu tombol yang men-toggle atribut itu.
    3. Menantang, Buat token semantik --color-border-soft yang bernilai #e5e7eb di mode terang dan #334155 di mode gelap, lalu pakai di daftar divide-y.
    Lihat solusi
    1. 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>
    
    1. 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>
    
    1. 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

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.