nextjs•themes

Next.js 16'da Bağımlılık Olmadan Açık ve Koyu Tema Uygulamak

Tailwind CSS ile Next.js 16'da açık ve koyu tema sistemi oluşturmak için bağımlılıksız bir yaklaşım.

Next.jsTailwind CSSReactTypeScriptThemeDark Mode
By Hamid Mohamadi2026-08-31

Next.js 16'da Bağımlılık Olmadan Açık ve Koyu Tema Uygulamak

Modern bir web uygulaması hem açık hem de koyu ortamlarda rahat bir deneyim sunmalıdır.

Tema desteği artık çoğu modern uygulamada beklenen bir özellik haline gelmiştir. Kullanıcılar arayüzün işletim sistemi tercihleriyle veya kendi kişisel seçimleriyle uyumlu olmasını ister.

Birçok geliştirici @wrksz/themes gibi kütüphaneler kullanır, ancak bir tema sisteminin dahili olarak nasıl çalıştığını anlamak daha iyi uygulamalar geliştirmemize yardımcı olur.

Bu makalede herhangi bir harici tema kütüphanesi yüklemeden Next.js 16'da eksiksiz bir açık ve koyu tema sistemi oluşturacağız.

Ne Oluşturacağız

Tema sistemimiz şunları destekleyecek:

Son akış:

Kullanıcı temayı değiştirir
        |
        v
Theme Provider durumu günceller
        |
        v
HTML sınıfı değişir
        |
        v
CSS değişkenleri güncellenir
        |
        v
Arayüz otomatik olarak değişir

Proje Teknoloji Yığını

Bu uygulama şunları kullanır:

Tema Değiştirme Nasıl Çalışır

Bir tema sistemi, kök HTML elementi üzerindeki sınıfları değiştirme mantığına dayanır.

<html class="dark">

dark sınıfı mevcut olduğunda Tailwind koyu mod stillerini uygular.

Bu sınıf kaldırıldığında uygulama açık moda geri döner.

Sistem Tercihini Algılama

Tarayıcılar işletim sistemi temasını algılamak için yerleşik bir API sağlar.

match-media.ts
window.matchMedia('(prefers-color-scheme: dark)');

Bu, uygulamamızın kullanıcının sistem temasını takip etmesini sağlar.

CSS Değişkenlerini Oluşturma

Tema sistemimizin temeli CSS değişkenleridir.

globals.css
:root {
  --background: #ffffff;
  --foreground: #171717;
}
.dark {
  --background: #0a0a0a;
  --foreground: #ededed;
}

Aynı CSS değişkeni adları yeniden kullanılır, ancak değerleri aktif temaya göre değişir.

Değişkenleri Tailwind ile Bağlama

globals.css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
}

Artık bileşenler semantik renkleri kullanabilir:

<div className="bg-background text-foreground">Content</div>

Theme Türünü Oluşturma

theme.ts
export type Theme = 'light' | 'dark' | 'system';

Theme Provider Oluşturma

Provider, mevcut tema durumunu yönetir.

theme-provider.tsx
'use client';
 
import { createContext, useContext, useEffect, useState } from 'react';
 
import type { Theme } from '@/lib/theme';
 
type ThemeContextType = {
  theme: Theme;
  setTheme: (theme: Theme) => void;
};
 
const ThemeContext = createContext<ThemeContextType | null>(null);
 
function getStoredTheme(): Theme {
  if (typeof window === 'undefined') {
    return 'system';
  }
 
  const stored = localStorage.getItem('theme');
 
  if (stored === 'light' || stored === 'dark' || stored === 'system') {
    return stored;
  }
 
  return 'system';
}
 
function applyTheme(theme: Theme) {
  const root = document.documentElement;
 
  root.classList.remove('light', 'dark');
 
  if (theme === 'system') {
    const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
 
    root.classList.add(dark ? 'dark' : 'light');
 
    return;
  }
 
  root.classList.add(theme);
}
 
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(getStoredTheme);
 
  useEffect(() => {
    applyTheme(theme);
 
    localStorage.setItem('theme', theme);
  }, [theme]);
 
  return (
    <ThemeContext.Provider
      value={{
        theme,
        setTheme: setThemeState,
      }}
    >
      {children}
    </ThemeContext.Provider>
  );
}
 
export function useTheme() {
  const context = useContext(ThemeContext);
 
  if (!context) {
    throw new Error('useTheme must be inside ThemeProvider');
  }
 
  return context;
}

Temayı HTML'e Uygulama

Aktif tema, kök elemente bir sınıf eklenerek kontrol edilir.

apply-theme.ts
document.documentElement.classList.remove('light', 'dark');
 
document.documentElement.classList.add('dark');

Kullanıcı Tercihini Kaydetme

Kullanıcı seçimi sayfa yenilendikten sonra da korunmalıdır.

theme-storage.ts
localStorage.setItem('theme', 'dark');

Okuma:

const theme = localStorage.getItem('theme');

Sistem Temasını Yönetme

system-theme.ts
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

Hydration Problemi

Next.js önce HTML'i sunucuda render eder.

Sunucunun şunlara erişimi yoktur:

Bu durum hydration mismatch hatalarına yol açabilir.

Örnek:

Sunucu:

<button>
  Sun
</button>

İstemci:

<button>
  Moon
</button>

Hydration Sorunlarını Çözme

İstemciye bağlı kullanıcı arayüzü, uygulama istemci üzerinde mount edilene kadar beklemelidir.

Mount durumunu useState ve useEffect ile manuel olarak takip etmek yerine React'in useSyncExternalStore özelliğini kullanarak yeniden kullanılabilir bir hook oluşturabiliriz.

use-mounted.ts
'use client';
 
import { useSyncExternalStore } from 'react';
 
const emptySubscribe = () => {
  return () => {};
};
 
export function useMounted() {
  return useSyncExternalStore(
    emptySubscribe,
    () => true,
    () => false,
  );
}

Bu hook şunları döndürür:

Bu, React hydration mismatch hatalarını önler.

Örnek:

Sunucu:

<button>
  Sun
</button>

İstemci:

<button>
  Moon
</button>

HTML yapısı aynıdır, ancak içerik farklıdır. React bu farkı hydration sırasında algılar.

Mounted Hook'u Kullanma

Bu hook, tarayıcıya özel özelliklere bağlı olan bileşenlerde kullanılabilir.

theme-toggle.tsx
'use client';
 
import { useMounted } from '@/hooks/use-mounted';
 
export function ThemeToggle() {
  const mounted = useMounted();
 
  if (!mounted) {
    return null;
  }
 
  return <button>Dark Mode</button>;
}

Böylece sunucu kararlı bir sonuç render eder ve istemci hydration sonrasında etkileşimli tema kontrollerini render eder.

Theme Toggle Oluşturma

theme-toggle.tsx
'use client';
 
import { Moon, Sun } from 'lucide-react';
 
import { useTheme } from './theme-provider';
import { useMounted } from '@/hooks/use-mounted';
 
export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
 
  const mounted = useMounted();
 
  if (!mounted) {
    return (
      <button className="rounded-full border p-3" aria-label="Theme toggle" />
    );
  }
 
  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="hover:bg-muted rounded-full border p-3 transition"
      aria-label="Theme toggle"
    >
      {theme === 'dark' ? <Moon size={20} /> : <Sun size={20} />}
    </button>
  );
}

Basitlik açısından bu toggle yalnızca açık ve koyu modlar arasında geçiş yapar. Production uygulamasında sistem tercihi için üçüncü bir seçenek de sunulabilir.

Projenin Son Yapısı

components/
└── theme/
    ├── theme-provider.tsx
    └── theme-toggle.tsx
 
hooks/
└── use-mounted.ts
 
lib/
└── theme.ts
 
app/
└── layout.tsx
 
globals.css

Sorumluluklar

BölümSorumluluk
CSS VariablesTema renklerini saklamak
Theme ProviderTema durumunu yönetmek
localStorageKullanıcı seçimini saklamak
matchMediaSistem temasını algılamak
useMounted HookHydration mismatch sorununu önlemek
Toggle ComponentKullanıcı etkileşimi

Sonuç

Bağımlılık kullanmadan bir tema sistemi oluşturmak, @wrksz/themes gibi kütüphanelerin dahili olarak nasıl çalıştığını anlamamıza yardımcı olur.

Öğrenme projeleri ve küçük uygulamalar için özel bir implementasyon hafiftir ve tam kontrol sağlar.

Önemli olan, kullandığımız araçların arkasındaki mekanizmayı anlamaktır.