React'te Dark Mode Nasıl Yapılır? FOUC Olmadan Profesyonel Çözüm

React'te dark mode nasıl eklenir? CSS variables, localStorage, prefers-color-scheme ve FOUC (beyaz ekran flash) sorununu çözen, production-ready dark...

Sorun: Sayfa Yüklenirken Beyaz Ekran Yanıp Sönüyor

Dark mode eklediniz, localStorage’a kaydettiniz — ama sayfa her yüklendiğinde önce beyaz, sonra siyah oluyor. Bu soruna FOUC (Flash of Unstyled Content) denir ve doğru çözülmezse kullanıcı deneyimini mahveder.

Dark Mode’un 3 Katmanı

Profesyonel bir dark mode implementasyonu 3 katmandan oluşur:

1. CSS Variables   → Renkleri tanımla
2. Head Script     → FOUC'u engelle (render-blocking)
3. React Toggle    → Kullanıcı etkileşimi

Katman 1: CSS Variables ile Tema Sistemi

Tüm renkleri CSS variable olarak tanımlayın. Tema değiştirmek = sadece class değiştirmek.

/* global.css */
:root {
  --color-bg: #ffffff;
  --color-text: #1a1a2e;
  --color-text-muted: #6b7280;
  --color-bg-muted: #f1f3f5;
  --color-border: #e5e7eb;
  --color-accent: #3b82f6;
}

[data-theme="dark"] {
  --color-bg: #0d1117;
  --color-text: #e6edf3;
  --color-text-muted: #8b949e;
  --color-bg-muted: #21262d;
  --color-border: #30363d;
  --color-accent: #58a6ff;
}

body {
  background-color: var(--color-bg);
  color: var(--color-text);
  transition: background-color 0.2s, color 0.2s;
}

Neden class yerine data-theme attribute?

  • CSS specificity sorunları yaratmaz
  • JavaScript’te dataset.theme ile kolay erişim
  • Nested tema desteği (bir bölüm farklı tema)

Katman 2: FOUC’u Engelleyen Head Script

Bu, en kritik adım. React mount olmadan ÖNCE temayı uygularsınız:

<!-- index.html veya layout'un <head> bölümüne -->
<script>
  (function() {
    var stored = localStorage.getItem('theme');
    var system = window.matchMedia('(prefers-color-scheme: dark)').matches;
    
    if (stored === 'dark' || (!stored && system)) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  })();
</script>

Neden çalışır?

  • Bu script render-blocking’tir (async/defer yok)
  • Tarayıcı sayfa çizmeden ÖNCE çalıştırır
  • React hiç mount olmadan tema doğru uygulanır
  • Sonuç: sıfır beyaz flash

Katman 3: React Toggle Bileşeni

// useTheme.ts
import { useState, useEffect } from 'react';

type Theme = 'light' | 'dark';

export function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'light';
    return (localStorage.getItem('theme') as Theme) || 
           (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  });

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
    localStorage.setItem('theme', theme);
  }, [theme]);

  // Sistem tercihi değişirse otomatik güncelle
  useEffect(() => {
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = (e: MediaQueryListEvent) => {
      if (!localStorage.getItem('theme')) {
        setTheme(e.matches ? 'dark' : 'light');
      }
    };
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, []);

  const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark');

  return { theme, toggle };
}
// ThemeToggle.tsx
import { useTheme } from './useTheme';

export function ThemeToggle() {
  const { theme, toggle } = useTheme();

  return (
    <button 
      onClick={toggle}
      aria-label={`${theme === 'dark' ? 'Açık' : 'Koyu'} temaya geç`}
    >
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  );
}

Öncelik Sırası — Ne Zaman Hangi Tema?

1. localStorage'da kayıtlı tercih varsa → onu kullan
2. Yoksa → sistem tercihine bak (prefers-color-scheme)
3. İkisi de yoksa → varsayılan light

SSR/SSG Uyumluluğu (Next.js, Astro)

SSR’da window ve localStorage yoktur. Çözüm:

// Astro'da: BaseHead.astro
<script is:inline>
  (function() {
    var s = localStorage.getItem('theme');
    var d = window.matchMedia('(prefers-color-scheme: dark)').matches;
    document.documentElement.setAttribute('data-theme', 
      s === 'dark' || (!s && d) ? 'dark' : 'light');
  })();
</script>
// Next.js'te: layout.tsx
<html suppressHydrationWarning>
  <head>
    <script dangerouslySetInnerHTML={{ __html: `
      (function() {
        var s = localStorage.getItem('theme');
        var d = window.matchMedia('(prefers-color-scheme: dark)').matches;
        document.documentElement.setAttribute('data-theme', 
          s === 'dark' || (!s && d) ? 'dark' : 'light');
      })();
    `}} />
  </head>

Sık Yapılan Hatalar

  1. useEffect içinde tema uygulamak: React mount gecikmesi → beyaz flash
  2. body class yerine inline style: Specificity sorunları yaratır
  3. Transition ilk yüklemede: İlk yüklemede transition: none olmalı, sonra etkinleştirin
  4. Sistem tercihini dinlememek: Kullanıcı OS’ta dark mode’u açarsa siteniz de değişmeli

İlgili Yazılar


Bu rehber MDN’nin prefers-color-scheme ve React’in useEffect dokümantasyonuna dayanarak hazırlanmıştır.

📚 İlgili Yazılar