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.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:
- Açık mod
- Koyu mod
- Sistem tercihi
- Kullanıcı seçimini kalıcı olarak saklama
- Tailwind CSS entegrasyonu
- Hydration güvenli render
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şirProje Teknoloji Yığını
Bu uygulama şunları kullanır:
- Next.js 16 App Router
- React
- TypeScript
- Tailwind CSS v4
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.
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.
: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
@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
export type Theme = 'light' | 'dark' | 'system';Theme Provider Oluşturma
Provider, mevcut tema durumunu yönetir.
'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.
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.
localStorage.setItem('theme', 'dark');Okuma:
const theme = localStorage.getItem('theme');Sistem Temasını Yönetme
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;Hydration Problemi
Next.js önce HTML'i sunucuda render eder.
Sunucunun şunlara erişimi yoktur:
- window
- localStorage
- tarayıcı tercihleri
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 client';
import { useSyncExternalStore } from 'react';
const emptySubscribe = () => {
return () => {};
};
export function useMounted() {
return useSyncExternalStore(
emptySubscribe,
() => true,
() => false,
);
}Bu hook şunları döndürür:
falsesunucu render'ı sırasındatrueistemcide hydration sonrasında
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.
'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
'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.cssSorumluluklar
| Bölüm | Sorumluluk |
|---|---|
| CSS Variables | Tema renklerini saklamak |
| Theme Provider | Tema durumunu yönetmek |
| localStorage | Kullanıcı seçimini saklamak |
| matchMedia | Sistem temasını algılamak |
| useMounted Hook | Hydration mismatch sorununu önlemek |
| Toggle Component | Kullanı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.

