Hero bölümünün ortasında ince bir ayırıcı çizgi var. Dark temada aylarca parlak gri göründü ve ben bunu "biraz açık kalmış" diye geçtim. Meğer o sınıf hiç var olmuyormuş. Tailwind'i kendi binary'siyle derleyip çıktıyı okuyunca anladım.
Hata: Olmayan Opaklık Değerleri
Tailwind'in varsayılan opaklık ölçeği beşin katlarıdır: 0, 5, 10, 15, 20 … 95, 100. Yani `bg-white/10` üretilir, `bg-white/8` üretilmez.
Üretilmediğinde ne olur? Hata almazsınız. Uyarı da almazsınız. Sınıf HTML'de durur, CSS'te karşılığı yoktur ve tarayıcı onu yok sayar.
// Bu satırdaki dark: sınıfı HİÇ CSS üretmiyordu
<div className="bg-gradient-to-r from-transparent dark:via-white/8 via-slate-200 to-transparent" />
// Sonuç: dark temada via rengi via-slate-200'e (#e2e8f0) düşüyor,
// yani neredeyse beyaz, tam opak bir çizgi çiziliyor.Sinsi kısmı şu: `dark:` öneki bir override'dır, fallback değil. `dark:via-white/8` üretilmeyince dark temada boşluk kalmıyor — bir alttaki `via-slate-200` yürürlükte kalıyor. Yani karanlık tema, açık tema rengini kullanıyor.
Bunu doğrulamak için projenin kendi Tailwind sürümüyle sadece o sınıfları derledim:
# Sadece test edilecek sınıfları içeren bir dosya oluştur, derle, çıktıya bak
echo '<div class="bg-white/8 bg-white/10 text-white/42 border-white/12"></div>' > /tmp/t.html
npx tailwindcss -i /tmp/in.css -o /tmp/out.css --content /tmp/t.html
grep -o 'bg-white\\/[0-9]*' /tmp/out.css
# çıktı: bg-white\/10 ← sadece bu. /8, /42, /12 yok.Tüm projede taradım: `/8`, `/12`, `/14`, `/16`, `/18`, `/42`, `/48`, `/74`, `/88`. Sekiz dosyada, yirmiye yakın yerde. Bunlardan ikisi gerçekten görünür hataydı — blog sayfasındaki içindekiler paneli açık temada tamamen zeminsiz kalıyordu ve spot metin karanlıkta 2.4:1 kontrasta düşüyordu.
İki Adımlı Düzeltme
Anlık çözüm köşeli parantez: `dark:via-white/[0.08]`. Bu her zaman üretilir çünkü keyfi değer olarak işleniyor.
Kalıcı çözüm ölçeği genişletmek. Kullandığım ara değerleri tanımladım, böylece hem `/8` çalışıyor hem de ileride sessizce kırılmıyor:
theme: {
extend: {
opacity: {
3: '0.03', 4: '0.04', 6: '0.06', 7: '0.07', 8: '0.08',
12: '0.12', 14: '0.14', 16: '0.16', 18: '0.18',
22: '0.22', 42: '0.42', 48: '0.48', 74: '0.74', 88: '0.88',
},
},
}İpucu
Bu sınıfa giren başka bir tuzak daha var: dinamik sınıf adı. `` className={`bg-${color}-500`} `` hiçbir zaman çalışmaz, çünkü Tailwind kaynak dosyalarını metin olarak tarıyor ve o birleştirmeyi çözemiyor. Renk değişkense `style` özniteliğini kullanın, sınıf üretmeye çalışmayın.
Asıl Sorun Daha Derindeydi
Bu hata bir semptomdu. Gerçek sorun şuydu: sitede tanımlı bir yüzey ölçeği yoktu.
Kart renklerini tek tek saydım — bileşenlerin içine elle yazılmış 13 farklı koyu hex vardı. `#0c0b18`, `#0c0e17`, `#0a0a12`, `#07060f`, `#0a0814`, `#0e0c1a`, `#09101a`, `#0e1117`... Bunların çoğu aynı hiyerarşi seviyesindeki kartlardı ve gözle bakınca "birinde bir tuhaflık var" diyordunuz ama hangisinde olduğunu söyleyemiyordunuz.
Üstelik üç farklı sayfa zemini vardı: ana sayfa sıcak bej, blog soğuk mavi-gri, 404 başka bir mavi-gri. Ana sayfadan bir yazıya geçince zemin rengi değişiyordu.
Çözüm: dört kademeli tek bir yüzey ölçeği ve tek zemin.
:root {
/* Kademe: page (zemin) → card (kart) → raised (kart üstü) → sunken (oyuk) */
--surface-page: #f3f1eb;
--surface-card: #fdfcf9;
--surface-raised: #ffffff;
--surface-sunken: #eceae3;
--line: rgba(120, 110, 90, 0.22);
--line-strong: rgba(120, 110, 90, 0.34);
}
html.dark {
--surface-page: #04070d;
--surface-card: #0b0e18;
--surface-raised: #121726;
--surface-sunken: #06090f;
--line: rgba(148, 163, 184, 0.13);
--line-strong: rgba(148, 163, 184, 0.22);
}Sonra bu değişkenleri Tailwind'e renk olarak tanıttım. Böylece bileşenlerde `bg-card` yazabiliyorum ve `dark:` öneki hiç gerekmiyor — değişken zaten temaya göre değişiyor.
colors: {
page: 'var(--surface-page)',
card: 'var(--surface-card)',
raised: 'var(--surface-raised)',
sunken: 'var(--surface-sunken)',
hairline: 'var(--line)',
},Bir kartın zemin tanımı
Önce
bg-white dark:bg-[#0c0e17]
Sonra
bg-card
İki sınıf yerine bir sınıf; ve daha önemlisi, kart rengini değiştirmek istediğimde 13 dosyayı değil bir CSS değişkenini düzenliyorum.
Açık Tema Neden Hep Üvey Evlat
Karanlık temayı tasarlarken saatler harcamıştım. Açık temayı ise "aynısının tersi" gibi düşünmüştüm. Kontrast ölçünce ortaya çıkan tablo şuydu:
| Eleman | Ölçülen | Gereken (AA) |
|---|---|---|
| Kart kenarlığı | 1.12 : 1 | 3 : 1 |
| Künye metni (tarih, süre) | 2.2 : 1 | 4.5 : 1 |
| İkincil buton kenarı | 1.4 : 1 | 3 : 1 |
Kartların kenarı yoktu. Bej zemin üstünde `rgba(180,170,150,0.2)` bir kenarlık, gözle bakınca "var gibi" ama gerçekte yok. Kenarlık renklerini koyulaştırdım ve künye metinlerini bir kademe koyulaştırdım.
Karanlık temada yumuşak görünen bir gri, açık temada görünmez oluyor. Aynı opaklık değerini iki temada kullanmak, iki temayı da yarım yapmak demek.
Cam efekti (`backdrop-filter`) için de aynı şey geçerli. Koyu zeminde saydam beyaz bir yüzey doğal duruyor; açık zeminde aynı yüzey kayboluyor. Bu yüzden `.glass` sınıfının iki tema için iki ayrı tanımı var ve ikisinin ortak yanı sadece bulanıklık miktarı.
Titremeyi Önlemek
Sunucu, kullanıcının hangi temada olduğunu bilmez. Bu yüzden ilk render ile istemci render'ı uyuşmaz ve React uyarı verir.
`next-themes` bunu `<html>` etiketine sınıf yazan küçük bir script ile çözüyor, ama React'in uyuşmazlık uyarısını susturmak sizin işiniz:
<html lang="tr" className={`${manrope.variable} ${mono.variable}`} suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</body>
</html>İkinci kısım daha önemli: tema durumunu OKUYAN her bileşenin monte olmayı beklemesi gerekiyor. Tema düğmesini `mounted` kontrolü olmadan render ederseniz, bir an yanlış ikon görünüp sonra değişiyor.
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Düğmeyi hiç render etmemek yerine yerini tutan bir kutu bırakın —
// aksi hâlde başlık çubuğu bir anlığına kayıyor.
if (!mounted) return <span className="block h-8 w-8" />Dikkat
Bu bekleme yalnızca `resolvedTheme` değerini KULLANAN bileşenler için gerekli. `dark:` sınıflarıyla çalışan her şey CSS seviyesinde hallolduğu için beklemeye ihtiyaç duymaz. Her bileşene `mounted` guard'ı koymak, sayfanın yarısını ilk boyamada boş bırakır.
Regresyonu Nasıl Yakalarım
Bu hatayı bir kere temizledim ama yarın yine `/7` yazabilirim. Bu yüzden basit bir kontrol ekledim — ölçekte olmayan bir opaklık değeri görürse derlemeyi durduruyor.
# scripts/check-opacity.sh
grep -roE '(bg|text|border|via|from|to|ring|divide)-[a-z]+(-[0-9]{2,3})?/[0-9]+' app components \
| grep -vE '/(0|5|10|15|20|25|30|35|40|45|50|55|60|65|70|75|80|85|90|95|100|3|4|6|7|8|12|14|16|18|22|42|48|74|88)$' \
&& { echo 'Tailwind ölçeğinde olmayan opaklık değeri'; exit 1; } || exit 0Çıkardığım Ders
Bir CSS aracının sessizce hiçbir şey üretmemesi, en zor fark edilen hata türü. Derleyici uyarmıyor, tarayıcı uyarmıyor, ekranda bir şey görünüyor — sadece yanlış şey görünüyor.
Bugün olsa iki şeyi baştan yapardım: yüzey ölçeğini ilk günden CSS değişkeni olarak tanımlar ve iki temanın kontrastını tasarım aşamasında ölçerdim. İkisi de bittikten sonra düzeltmesi, baştan yapmaktan uzun sürdü.