React'ta Dinamik Renk Temalama: CSS Değişkenlerinin Ötesinde
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
Çoğu temalama öğreticisi karanlık mod geçişinde durur. <html> üzerinde bir sınıf değiştirir, bir avuç CSS değişkenini döndürürsünüz, bitti. Bu yaygın bir durumu kapsar ama daha ilginç sorunu kaçırır: peki kullanıcılar kendi marka renklerini seçebiliyorsa? SaaS ürününüz her biri kendi kimliğine sahip birden fazla istemciye hizmet ediyorsa? Tema, çalışma zamanında tek bir giriş hex değerinden eksiksiz harmonik bir palet oluşturması gerekiyorsa?
Dinamik renk temalama tam burada başlar — ve yalnızca CSS değişkenleri yeterli değildir. Çalışma zamanı renk algoritmalarına, navigasyonda hayatta kalan React durumuna, kalıcılık stratejilerine ve bir kaydırıcı her hareket ettiğinde tüm bileşen ağacınızı yeniden oluşturmayan bir mimariye ihtiyacınız vardır.
Bu rehber bu katmanların her birini derinlemesine ele alır.
React Temaları için CSS Değişken Yaklaşımı
Temel: Semantik Token Mimarisi
Her dinamik tema sistemi aynı temelle başlar: renk değeri yerine semantik rollerine göre adlandırılmış CSS özel özellikleri. Bir değişkeni --blue-500 olarak adlandırmayın. --brand-primary olarak adlandırın. Değer değişebilir; rol değişemez:
/* globals.css */
:root {
/* Semantik tokenlar — bileşenlerin kullandığı bunlardır */
--bg-base: #F8FAFC;
--bg-surface: #FFFFFF;
--bg-sunken: #F1F5F9;
--text-primary: #1E293B;
--text-secondary: #64748B;
--text-disabled: #94A3B8;
--brand-primary: #2563EB;
--brand-hover: #1D4ED8;
--brand-subtle: #EFF6FF;
--on-brand: #FFFFFF;
--border-default: #E2E8F0;
--border-strong: #CBD5E1;
--status-error: #DC2626;
--status-success: #16A34A;
--status-warning: #D97706;
}
Bileşenler, ham renkler değil tokenları referans alır:
.card {
background: var(--bg-surface);
border: 1px solid var(--border-default);
color: var(--text-primary);
}
.btn-primary {
background: var(--brand-primary);
color: var(--on-brand);
}
JavaScript'te --brand-primary'yi güncellediğinizde, onu referans alan her bileşen anında güncellenir — React yeniden oluşturma olmadan, prop drilling olmadan, bağlam abonelikleri olmadan. CSS kaskadı bunu yönetir.
JavaScript'ten Token Yazma
document.documentElement.style.setProperty('--brand-primary', '#7C3AED');
document.documentElement.style.setProperty('--brand-hover', '#6D28D9');
document.documentElement.style.setProperty('--brand-subtle', '#EDE9FE');
document.documentElement.style.setProperty('--on-brand', '#FFFFFF');
Renk Algoritmalarıyla Çalışma Zamanı Tema Üretimi
Tam Token Setini Üretme
Marka hex'inden ihtiyacınız olan tüm tokenları üreten eksiksiz bir fonksiyon:
import chroma from 'chroma-js';
function generateThemeTokens(brandHex) {
const brand = chroma(brandHex);
// WCAG göreceli parlaklığa göre marka metni siyah mı beyaz mı olacak
const brandLuminance = brand.luminance();
const onBrand = brandLuminance > 0.179 ? '#000000' : '#FFFFFF';
// OKLCH'de algısal olarak düzgün adımlarla açık tondan koyu tona ölçek
const scale = chroma.scale([
chroma(brandHex).brighten(2.5).desaturate(0.5).hex(),
brandHex,
chroma(brandHex).darken(2.5).hex(),
]).mode('oklch').colors(11);
return {
'--brand-primary': brandHex,
'--brand-hover': chroma(brandHex).darken(0.5).hex(),
'--brand-active': chroma(brandHex).darken(1).hex(),
'--brand-subtle': scale[1],
'--brand-muted': scale[2],
'--on-brand': onBrand,
'--brand-50': scale[0],
'--brand-100': scale[1],
'--brand-500': scale[5],
'--brand-900': scale[9],
'--brand-950': scale[10],
};
}
function applyTheme(brandHex) {
const tokens = generateThemeTokens(brandHex);
const root = document.documentElement;
Object.entries(tokens).forEach(([prop, value]) => {
root.style.setProperty(prop, value);
});
}
Erişilebilir Renk Çiftleri
function ensureContrast(foreground, background, minRatio = 4.5) {
let color = chroma(foreground);
let ratio = chroma.contrast(color.hex(), background);
let iterations = 0;
while (ratio < minRatio && iterations < 20) {
color = color.darken(0.2);
ratio = chroma.contrast(color.hex(), background);
iterations++;
}
return color.hex();
}
// Beyaz arka planda marka renkli metin etiketi için
const safeLabel = ensureContrast('#FFD700', '#FFFFFF');
Tema Durum Yönetimi için React Context
ThemeContext Mimarisi
// contexts/ThemeContext.tsx
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from 'react';
import { generateThemeTokens } from '../lib/theme-generator';
interface ThemeState {
brandHex: string;
mode: 'light' | 'dark' | 'system';
}
interface ThemeContextValue extends ThemeState {
setBrand: (hex: string) => void;
setMode: (mode: 'light' | 'dark' | 'system') => void;
resolvedMode: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
const STORAGE_KEY = 'app-theme';
function getStoredTheme(): ThemeState {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) return JSON.parse(stored) as ThemeState;
} catch {}
return { brandHex: '#2563EB', mode: 'system' };
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<ThemeState>(getStoredTheme);
const systemDark =
typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false;
const resolvedMode: 'light' | 'dark' =
state.mode === 'system'
? systemDark ? 'dark' : 'light'
: state.mode;
useEffect(() => {
const tokens = generateThemeTokens(state.brandHex);
const root = document.documentElement;
Object.entries(tokens).forEach(([prop, value]) => {
root.style.setProperty(prop, value as string);
});
root.setAttribute('data-theme', resolvedMode);
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, [state, resolvedMode]);
const setBrand = useCallback((hex: string) => {
setState(prev => ({ ...prev, brandHex: hex }));
}, []);
const setMode = useCallback((mode: 'light' | 'dark' | 'system') => {
setState(prev => ({ ...prev, mode }));
}, []);
return (
<ThemeContext.Provider value={{ ...state, setBrand, setMode, resolvedMode }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
Renk Seçici Bileşeni
// components/BrandColorPicker.tsx
import { useTheme } from '../contexts/ThemeContext';
import { useState, useRef } from 'react';
export function BrandColorPicker() {
const { brandHex, setBrand } = useTheme();
const [localHex, setLocalHex] = useState(brandHex);
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalHex(value);
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
setBrand(value);
}
}, 150);
};
return (
<div className="flex items-center gap-3">
<label htmlFor="brand-picker" className="text-sm font-medium">
Marka Rengi
</label>
<input
id="brand-picker"
type="color"
value={localHex}
onChange={handleChange}
/>
<input
type="text"
value={localHex}
onChange={handleChange}
className="w-24 px-2 py-1 text-sm font-mono border rounded"
placeholder="#2563EB"
/>
</div>
);
}
150ms gecikme kritiktir — onsuz, hex girişine yazarken generateThemeTokens her tuş vuruşunda çalışır.
Tailwind CSS Dinamik Renk Sınıfları
CSS Değişken Köprüsü
/* styles.css (Tailwind v4) */
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary);
--color-brand-hover: var(--brand-hover);
--color-brand-subtle: var(--brand-subtle);
--color-on-brand: var(--on-brand);
}
Artık bg-brand, text-brand ve hover:bg-brand-hover, çalışma zamanında --brand-primary'nin tuttuğu değeri yansıtan geçerli Tailwind sınıflarıdır:
<button className="bg-brand text-on-brand hover:bg-brand-hover px-4 py-2 rounded-lg">
Birincil Eylem
</button>
Gerçekten Dinamik Renkler için Satır İçi Stiller
// Gerçekten dinamik renkler için doğru:
<div
style={{ backgroundColor: swatch.hex }}
className="w-8 h-8 rounded"
/>
// Bu çalışma zamanında ÇALIŞMAZ (sınıf stil sayfasında yok):
<div className={`bg-[${swatch.hex}]`} /> // Yanlış
Kullanıcı Renk Tercihlerini Kalıcı Hale Getirme
Oturumlar Arasında Kalıcılık için localStorage
const [state, setState] = useState<ThemeState>(() => {
if (typeof window === 'undefined') {
return { brandHex: '#2563EB', mode: 'system' };
}
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : { brandHex: '#2563EB', mode: 'system' };
} catch {
return { brandHex: '#2563EB', mode: 'system' };
}
});
Yüklemede Varsayılan Tema Flaşını Önleme
// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="tr" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var stored = JSON.parse(localStorage.getItem('app-theme') || '{}');
var brand = stored.brandHex || '#2563EB';
var mode = stored.mode || 'system';
var resolvedMode = mode === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: mode;
document.documentElement.setAttribute('data-theme', resolvedMode);
document.documentElement.style.setProperty('--brand-primary', brand);
} catch(e) {}
})();
`,
}}
/>
</head>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
Erişilebilir Karanlık Mod Varyantları Üretme
function generateDarkTokens(brandHex) {
const brand = chroma(brandHex);
const hsl = brand.hsl();
const darkBrand = chroma.hsl(hsl[0], hsl[1] * 0.9, Math.max(0.55, hsl[2] + 0.15));
return {
'[data-theme="dark"]': {
'--brand-primary': darkBrand.hex(),
'--brand-hover': darkBrand.lighten(0.3).hex(),
'--brand-subtle': darkBrand.darken(2).desaturate(0.5).hex(),
'--on-brand': '#000000',
'--bg-base': '#0F172A',
'--bg-surface': '#1E293B',
'--text-primary': '#F1F5F9',
'--text-secondary': '#94A3B8',
},
};
}
Temel Çıkarımlar
- CSS özel özellikleri temelin temelidir — rol bazlı adlandırılmış semantik tokenlar, bileşenleri değiştirmeden tam tema değişikliklerine izin verir.
- Tek bir marka hex'inden çalışma zamanı token üretimi şunları gerektirir: bir parlaklık/karanlık skalası, marka metni için WCAG kontrast denetimi ve karanlık mod varyantları için ayrı hesaplama.
- React context tema durumunu yönetir (aktif marka hex, açık/karanlık mod) ve tokenları DOM'a yan etki olarak uygular.
- Renk seçiciler veya kaydırıcılar gibi sürekli girdilere bağlandığında pahalı renk hesaplamalarını (150ms) geciktirin.
- Tailwind CSS için:
bg-brandyardımcılarının çalışma zamanı token değerini yansıtması amacıyla@theme'de bir CSS değişken köprüsü tanımlayın. - Varsayılan tema flaşını
<head>'de oluşturmadan önce en kritik tokenları uygulayan senkron bir engelleme komut dosyasıyla önleyin. - SSR uygulamaları için kullanıcı başına tema için temayı çerezde saklayın; böylece sunucu doğru başlangıç HTML'sini oluşturabilir.
- Oluşturulan renk skalalarını doğrulamak için Ton Üreteci'ni ve tam tema paletindeki harmoniyi ve kontrast oranlarını doğrulamak için Palet Oluşturucu'yu kullanın.