Cara Membuat Dark Mode Next.js 14 Tailwind CSS yang Stabil & Tanpa Flicker

Pernahkah kamu mengunjungi sebuah website di malam hari, lalu tiba-tiba layar berubah menjadi putih terang yang menyilaukan mata hanya karena halaman tersebut belum mendukung dark mode? Sebagai developer, memberikan opsi mode gelap bukan lagi sekadar tren estetika, melainkan standar kenyamanan pengguna (user experience).

Namun, membuat fitur dark mode yang benar-benar stabil di framework modern seperti Next.js 14 tidak sesederhana hanya mengubah warna background. Kamu mungkin akan menghadapi masalah klasik seperti layout shift atau efek "flash" (flicker) di mana layar putih muncul sekejap sebelum berubah jadi gelap saat halaman di-refresh. Hal ini terjadi karena adanya ketidaksinkronan antara rendering di sisi server (SSR) dan client.

Dalam panduan ini, kita akan membedah tuntas cara membangun sistem dark mode yang solid menggunakan Next.js 14 App Router, Tailwind CSS, dan library next-themes. Mari kita mulai!


Mengapa Harus Menggunakan Next-Themes?

Mungkin kamu berpikir, "Kenapa tidak pakai useState biasa saja?" Jawabannya adalah efisiensi dan stabilitas. Next-themes adalah library yang dirancang khusus untuk ekosistem Next.js. Berikut adalah alasan mengapa kamu wajib menggunakannya:

  • Anti-Flicker: Menghilangkan masalah warna putih yang muncul sekilas saat reload.
  • Sinkronisasi System Preference: Secara otomatis mendeteksi apakah user menggunakan mode gelap atau terang di pengaturan OS mereka (Windows/macOS/Android/iOS).
  • Support Multi-Theme: Tidak hanya gelap dan terang, kamu bisa menambah tema lain seperti "Midnight" atau "Forest".
  • Tanpa Tambahan Berat: Ukurannya sangat kecil dan tidak membebani performa website kamu.

Langkah 1: Persiapan Project Next.js 14

Pastikan kamu sudah memiliki project Next.js 14 yang menggunakan Tailwind CSS. Jika belum, kamu bisa membuatnya dengan perintah berikut di terminal kamu:

npx create-next-app@latest my-dark-mode-app --typescript --tailwind --eslint

Setelah project siap, masuk ke folder tersebut dan instal library next-themes dan lucide-react (untuk ikon matahari dan bulan yang cantik):

npm install next-themes lucide-react

Langkah 2: Konfigurasi Tailwind CSS

Secara default, Tailwind CSS menggunakan strategi media untuk dark mode. Artinya, warna akan berubah hanya berdasarkan pengaturan sistem operasi user. Agar kita bisa mengontrolnya secara manual lewat tombol (toggle), kita perlu mengubahnya menjadi strategi class.

Buka file tailwind.config.ts (atau .js) kamu, lalu tambahkan properti darkMode:

import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  darkMode: 'class', // Tambahkan baris ini
  theme: {
    extend: {},
  },
  plugins: [],
};
export default config;

Dengan pengaturan class, Tailwind akan melihat apakah ada class .dark pada elemen html atau body. Jika ada, maka utility class bertanda dark: akan aktif.


Langkah 3: Membuat Theme Provider

Karena Next.js 14 menggunakan App Router secara default, semua komponen adalah Server Components. Namun, context untuk tema harus berjalan di sisi client. Kita perlu membuat pembungkus (wrapper) khusus agar aplikasi kita tahu tema apa yang sedang aktif.

Buat file baru di src/components/theme-provider.tsx:

"use client"

import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Perhatikan penggunaan directive "use client" di baris paling atas. Ini sangat krusial karena manipulasi tema melibatkan localStorage dan event listeners yang hanya ada di browser.


Langkah 4: Memasang Provider di Root Layout

Sekarang, kita harus membungkus seluruh aplikasi kita dengan ThemeProvider yang baru saja kita buat. Buka file src/app/layout.tsx.

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Next.js 14 Dark Mode",
  description: "Implementasi dark mode yang stabil",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Penting: Tambahkan atribut suppressHydrationWarning pada tag <html>. Kenapa? Karena next-themes akan memperbarui elemen tersebut. Tanpa atribut ini, React akan mengeluarkan peringatan (warning) di konsol karena ada perbedaan antara HTML dari server dan client saat proses hidrasi.


Langkah 5: Membuat Tombol Switcher (Theme Toggle)

Ini adalah bagian yang paling seru. Kita akan membuat tombol ikon yang bisa berubah antara mode terang dan gelap. Buat file di src/components/ThemeToggle.tsx.

"use client"

import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  const [mounted, setMounted] = React.useState(false)

  // Menghindari masalah hidrasi
  React.useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return <div className="w-9 h-9" /> // Placeholder agar layout tidak goyang
  }

  return (
    <button
      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
      className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
      aria-label="Toggle Dark Mode"
    >
      {theme === "dark" ? (
        <Sun className="h-5 w-5 text-yellow-400" />
      ) : (
        <Moon className="h-5 w-5 text-slate-700" />
      )}
    </button>
  )
}

Penjelasan Mengenai State mounted

Inilah rahasia agar website kamu tidak flicker. Karena server tidak tahu apa tema yang disimpan di localStorage user, kita tidak boleh merender ikon berdasarkan tema secara langsung di server. Dengan menunggu useEffect (yang hanya jalan di client), kita memastikan UI hanya diupdate setelah browser siap.


Langkah 6: Menggunakan Dark Mode di Komponen

Sekarang sistem kamu sudah siap! Untuk menggunakan dark mode di halaman kamu, kamu tinggal menggunakan prefix dark: dari Tailwind. Mari kita coba edit file src/app/page.tsx:

import { ThemeToggle } from "@/components/ThemeToggle";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24 bg-white dark:bg-slate-950 transition-colors duration-500">
      <div className="absolute top-5 right-5">
        <ThemeToggle />
      </div>

      <div className="text-center">
        <h1 className="text-4xl font-bold text-slate-900 dark:text-white">
          Dark Mode Berhasil Terpasang!
        </h1>
        <p className="mt-4 text-slate-600 dark:text-slate-400 max-w-md">
          Sekarang kamu bisa menikmati tampilan yang lebih nyaman di mata. 
          Gunakan tombol di pojok kanan atas untuk berpindah tema.
        </p>
      </div>

      <div className="mt-10 p-6 bg-gray-100 dark:bg-slate-900 rounded-xl border border-gray-200 dark:border-slate-800 shadow-sm">
        <p className="dark:text-slate-300">
          Card ini otomatis berubah warna mengikuti tema.
        </p>
      </div>
    </main>
  );
}

Mencegah Flash Warna Saat Loading

Meskipun kita sudah menggunakan next-themes, terkadang ada kasus ekstrem di mana warna latar belakang body masih "nge-flash". Untuk mencegahnya secara total, pastikan kamu mendefinisikan warna dasar di globals.css kamu menggunakan variabel CSS atau utility class yang tepat.

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --background: #ffffff;
  --foreground: #0f172a;
}

[class="dark"] {
  --background: #020617;
  --foreground: #f8fafc;
}

body {
  color: var(--foreground);
  background: var(--background);
  /* Tambahkan transisi halus agar perpindahan tidak kaku */
  transition: background-color 0.3s ease, color 0.3s ease;
}

Optimasi SEO dan Aksesibilitas

Membuat dark mode bukan cuma soal warna, tapi juga soal keterbacaan. Berikut adalah beberapa tips tambahan agar dark mode kamu lebih berkualitas:

  1. Kontras yang Cukup: Jangan gunakan hitam pekat (#000000) untuk background dan putih pekat untuk teks. Gunakan variasi abu-abu gelap (seperti slate-950) agar mata tidak cepat lelah.
  2. Warna Gambar: Terkadang gambar yang terlalu terang bisa merusak estetika dark mode. Kamu bisa menggunakan filter CSS untuk meredupkan gambar sedikit saat mode gelap: dark:brightness-75.
  3. Aria-Label: Pastikan tombol toggle kamu memiliki aria-label agar pengguna yang menggunakan pembaca layar (screen reader) tahu fungsi tombol tersebut.

Kesimpulan

Implementasi dark mode di Next.js 14 dengan Tailwind CSS sebenarnya sangat menyenangkan jika kamu tahu alat yang tepat. Dengan menggunakan next-themes, kamu sudah menyelesaikan 90% masalah teknis seperti sinkronisasi tema dan pencegahan flicker.

Sekarang kamu punya website yang modern, nyaman di mata, dan terlihat profesional. Jangan lupa untuk selalu mengetes fitur ini di berbagai browser dan perangkat untuk memastikan pengalaman pengguna yang konsisten.

Mau coba tantangan lebih? Coba tambahkan animasi transisi yang lebih kompleks menggunakan Framer Motion saat user menekan tombol toggle tema!

Selamat mencoba, dan semoga artikel ini bermanfaat buat project kamu selanjutnya!

Aurealisa ID
Fakta Gambar (Science Fact), Kutipan (Quote Of The Day), Ulasan (Science Review), Tips, Artikel.

Komentar