Modern Renk ve CSS Matematiği: oklch(), color-mix(), Relative Color, contrast-color() ve Math Function'ları
oklch() açıklığı insan gözünün algıladığı gibi ölçer; bu yüzden aynı L değerindeki sarı ve mavi aynı parlaklıkta görünür, HSL'de ise beyaz metnin kontrastı 1.07 ile 8.59 arasında değişir. Bookstore'un kategori renginden relative color, color-mix() ve alpha() ile tonlar türetiyor, contrast-color()'ın sınırını, round/mod/rem, progress() ve typed arithmetic'i ve Baseline olmayan if(), @function, random()'u ölçüyoruz.
Sürüm notu: Bu yazı CSS Color Level 4/5 ve CSS Values Level 4/5 spesifikasyonlarına ve Baseline'ın Kasım 2026 durumuna göre yazıldı; örnekler Chrome 158 ve WebKit 27.2 (Playwright 1.65) ile denenmiştir, Firefox desteği web-features 3.46 verisinden ve web.dev'in aylık "New to the web platform" yazılarından alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için kategori renklerini bir tasarımcı HSL (Hue, Saturation, Lightness; ton, doygunluk ve açıklıkla renk tarifi) ile seçmiş: hepsi aynı doygunlukta ve aynı açıklıkta, hsl(H 100% 50%), yalnız ton (hue) değişiyor. Fikir şu: lightness hepsinde 50% olduğuna göre üzerlerine yazılan beyaz metin hepsinde aynı okunurlukta olur. Lab sayfasında iki tanesini yan yana koydum:
/* 1) hsl'de ve oklch'de aynı "lightness" */
#hsl-yellow { background-color: hsl(60 100% 50%); }
#hsl-blue { background-color: hsl(240 100% 50%); }
#oklch-yellow { background-color: oklch(0.7 0.15 100); }
#oklch-blue { background-color: oklch(0.7 0.15 265); }
İlk ikisine bak: sarı hsl(60 100% 50%) ve mavi hsl(240 100% 50%). Beyaz metnin bu iki zemin üzerindeki kontrast oranı aynı mı, yakın mı, yoksa çok mu farklı?
Bir önceki yazıda Bookstore'a koyu tema ekledik ve CMS'ten (Content Management System; içerik yönetim sistemi) gelen kategori rengini bir custom property olarak sayfaya taşıdık. Bu yazıda o renklerin kendisine geçiyoruz: algısal açıklığı doğru ölçen oklch(), tek bir renkten palet türeten relative color ve color-mix(), yalnız saydamlığı değiştiren alpha(), metin rengini kendi seçen contrast-color() ve sınırları. İkinci yarıda CSS'in matematik fonksiyonları ve henüz Baseline olmayan if(), @function, random() var.
Hex'ten oklch'ye: Algısal Açıklık
node scripts/11-color.mjs
--- chromium
hsl-yellow #ffff00 luminance 0.928 contrast with white 1.07
hsl-blue #0000ff luminance 0.072 contrast with white 8.59
oklch-yellow #b59f00 luminance 0.346 contrast with white 2.65
oklch-blue #709afb luminance 0.335 contrast with white 2.73
base rgb(37, 99, 235) = #2563eb
oklch-of-hex oklch(0.54613 0.215194 262.885) = #2563eb
darker oklch(0.44613 0.215194 262.885) = #0442c8
soft oklch(0.95 0.0645582 262.885) = #d8efff
mix-black oklch(0.464211 0.182915 262.885) = #1c4ebd
mix-srgb color(srgb 0.572549 0.694118 0.460784) = #92b175
mix-oklch oklch(0.757057 0.213095 186.32) = #00d8c5
faded color(srgb 0.145098 0.388235 0.921569 / 0.15)
accent-light on #b91c1c: contrast-color() = rgb(255, 255, 255); black 3.25, white 6.47
accent-dark on #f87171: contrast-color() = rgb(0, 0, 0); black 7.59, white 2.77
royal-blue on #2277d3: contrast-color() = rgb(0, 0, 0); black 4.65, white 4.52
round(up, 0.9rem, 4px) = 16px, mod(-7px, 3px) = 2px, rem(-7px, 3px) + 10px = 9px, abs(-12px) * sign(-0.5) + 20px = 8px
480px shelf: --cols 2, progress(100cqi, 20rem, 40rem) 0.5
if() 200px, @function 42px, random() step 50px one of 100/150/200px
İlk iki satır tahmin sorusunun cevabı. İkisinin de HSL lightness'ı 50%, ama sarının bağıl parlaklığı (luminance) 0.928, mavininki 0.072. Beyaz metnin sarı üzerinde kontrastı 1.07:1, yani neredeyse görünmez; mavi üzerinde 8.59:1. HSL'in lightness'ı insan gözünün parlaklık algısıyla ilgili değil, sRGB kanallarının matematiksel bir dönüşümü. Göz sarıyı maviden çok daha parlak görür ve HSL bunu hesaba katmaz.
OKLCH (Oklab renk uzayının açıklık, kroma ve ton biçimi) bu sorunu çözmek için tasarlanmış bir renk uzayı. oklch(L C H)'de L algısal açıklık (0–1), C kroma, yani renklilik, H ise ton açısı. Üçüncü ve dördüncü satırlarda aynı L (0.7) ve aynı C (0.15) ile bir sarı ve bir mavi var: luminance değerleri 0.346 ve 0.335, beyazla kontrastları 2.65 ve 2.73. Ton değişti, algılanan açıklık neredeyse aynı kaldı. Bir paleti oklch'de kurduğunda "bu kategori ötekilerden daha koyu görünüyor" sorunu büyük ölçüde ortadan kalkıyor ve açıklığı değiştirmek istediğinde yalnız L'ye dokunuyorsun.
Var olan bir hex rengin oklch karşılığını görmek için tarayıcıya sorabilirsin. Bookstore'un Science Fiction mavisi #2563eb, oklch(from #2563eb l c h) ile çevrildiğinde oklch(0.54613 0.215194 262.885) çıkıyor (çıktıda oklch-of-hex) ve sRGB'ye geri çevrilince yine #2563eb. Bir uyarı: oklch, sRGB'nin gösterebileceğinden daha canlı renkleri de tarif edebilir. Yüksek kroma değerleri ekranın gamutu dışına düşerse tarayıcı rengi ekranın gösterebileceği en yakın renge indirir; bu yüzden C'yi tasarım aracında görerek seç.
Tek Renkten Palet: Relative Color ve color-mix()
Bir önceki yazıda kategori rengini --cat-color ile sayfaya taşıdık; editör tek bir renk seçiyor. Hover zemini, kenarlık, açık bir arka plan gibi tonları her kategori için ayrı ayrı istemek yerine bu tek renkten türetebiliriz:
/* 2) tek bir kategori renginden türetilmiş palet */
:root { --cat: #2563eb; }
#base { background-color: var(--cat); }
#oklch-of-hex { background-color: oklch(from var(--cat) l c h); }
#darker { background-color: oklch(from var(--cat) calc(l - 0.1) c h); }
#soft { background-color: oklch(from var(--cat) 0.95 calc(c * 0.3) h); }
#mix-black { background-color: color-mix(in oklch, var(--cat), black 15%); }
#mix-srgb { background-color: color-mix(in srgb, var(--cat), yellow); }
#mix-oklch { background-color: color-mix(in oklch, var(--cat), yellow); }
#faded { background-color: alpha(from var(--cat) / 15%); }
Relative color syntax'ı, oklch(from COLOR l c h) biçiminde bir rengi parçalarına ayırır; her kanalı bir değişken gibi kullanıp yeniden birleştirmeni sağlar. #darker açıklığı 0.1 düşürüyor (calc(l - 0.1)) ve sonuç #0442c8; ton ve kroma aynı. #soft açıklığı sabit 0.95'e çekiyor, kromayı %30'una indiriyor: #d8efff, kategori renginin çok açık bir zemini. Bu işlemleri oklch'de yaptığım için "0.1 daha koyu" her kategoride aynı algısal farkı veriyor.
color-mix() iki rengi verilen oranda ve verilen renk uzayında karıştırır. color-mix(in oklch, var(--cat), black 15%) rengi %15 siyaha yaklaştırıyor. Karıştırma uzayı sonucu doğrudan değiştiriyor: aynı mavi ile sarıyı sRGB'de karıştırınca grileşmiş bir yeşil (#92b175), oklch'de canlı bir camgöbeği (#00d8c5) çıkıyor. sRGB'de kanallar düz çizgi üzerinde ortalanıyor ve ara renkler soluklaşıyor; oklch'de ton açısı kısa yoldan dönüyor ve kroma korunuyor. Hover ve aktif tonları için oklch daha öngörülebilir sonuç veriyor; iki markanın renk geçişi gibi "gerçek bir karışım" istediğinde başka bir uzay seçebilirsin.
alpha() ve Bookstore'da Hover Tonları
Lab'deki son renk alpha(from var(--cat) / 15%): rengin kendisine dokunmadan yalnız saydamlığı değiştiriyor. Aynı şey relative color ile oklch(from var(--cat) l c h / 15%) diye de yazılabilir; alpha() yalnız daha kısa ve niyeti daha açık. Eylül 2026'da Baseline oldu. Bookstore'da iki yerde kullandım. Kategori bağlantılarının hover zemini artık kategorinin kendi renginden geliyor:
/* kategori renginden türetilen hover tonu: aynı renk, %15 opaklık */
.cat-link:hover {
background-color: alpha(from var(--cat-color, var(--color-muted)) / 15%);
}
Sepete ekle butonu ise şimdiye kadar hover'da hiç değişmiyordu: vendor kit'in .btn-primary:hover kuralı daha düşük bir layer'da kaldığı için, serinin ilk yazısında components layer'ında verdiğimiz kırmızı zemin onu yeniyordu. Hover tonunu da aynı accent'ten türettim:
.add-to-cart {
background-color: var(--color-accent);
}
/* koyu temada accent açık kırmızı: beyaz metin yalnız 2.77:1'e ulaşırdı */
@supports (color: contrast-color(red)) {
.add-to-cart {
color: contrast-color(var(--color-accent));
}
}
/* hover: aynı accent, oklch'de %15 siyaha karıştırılmış */
.add-to-cart:hover {
background-color: color-mix(in oklch, var(--color-accent), black 15%);
}
chromium light add-to-cart hover rgb(255, 255, 255) on oklch(0.429613 0.161935 27.519) | badge rgb(255, 255, 255) on rgb(185, 28, 28) | category hover color(srgb 0.145098 0.388235 0.921569 / 0.15)
chromium dark add-to-cart hover rgb(0, 0, 0) on oklch(0.604037 0.141252 22.2179) | badge rgb(0, 0, 0) on rgb(248, 113, 113) | category hover color(srgb 0.145098 0.388235 0.921569 / 0.15)
Bu dosyadaki ilk kural bir sonraki bölümün konusu. Script'in Bookstore'u ölçen son iki satırında açık temada hover zemini oklch(0.429613 …), koyu temada oklch(0.604037 …); ikisi de o temanın accent'inin %15 koyusu. Koyu tema için ayrı bir hover rengi yazmadım; token değişince türetilen renk de değişiyor.
contrast-color(): Siyah mı, Beyaz mı?
Koyu temada accent açık bir kırmızı (#f87171). Butonun beyaz metni bu zeminde yalnız 2.77:1 kontrast veriyor; WCAG'nin (Web Content Accessibility Guidelines; web erişilebilirlik yönergeleri) 2. sürümünün normal metin için istediği 4.5:1'in çok altında. contrast-color(COLOR) verilen renkle daha yüksek kontrast oluşturan rengi, beyazı ya da siyahı döndürür. Lab'de üç zemin üzerinde denedim:
/* 3) contrast-color() siyahı ya da beyazı seçer */
.on { color: contrast-color(var(--bg)); background-color: var(--bg); }
<p class="on" id="accent-light" style="--bg: #b91c1c">Bestseller</p>
<p class="on" id="accent-dark" style="--bg: #f87171">Bestseller</p>
<p class="on" id="royal-blue" style="--bg: #2277d3">Bestseller</p>
Çıktıdaki accent-light satırında açık temanın accent'i #b91c1c üzerinde beyaz seçildi (6.47:1); accent-dark'ta #f87171 üzerinde siyah (7.59:1). Bookstore'un rozeti ve butonu artık bu kararı kendisi veriyor; yukarıdaki Bookstore çıktısında açık temada beyaz, koyu temada siyah metin var.
Üçüncü satır sınırı gösteriyor. #2277d3 gibi orta tonda bir mavide siyahın kontrastı 4.65, beyazın 4.52; fonksiyon siyahı seçti ve iki değer de 4.5:1'i zar zor geçiyor. MDN (Mozilla'nın web dokümantasyon sitesi) de bu örneği veriyor ve siyahın küçük metinde okunur olmadığını belirtiyor. contrast-color() yalnız iki aday arasında daha iyisini seçer; seçilen rengin yeterli olduğunu garanti etmez. Üstelik spesifikasyon hangi kontrast algoritmasının kullanılacağını tarayıcıya bırakıyor. Orta tonlu zeminlerden kaçınmak, renkleri açık ya da koyu uçlardan seçmek ve sonucu ölçmek hâlâ senin işin. Kontrast hesabının ayrıntısı ve WCAG'nin bu konudaki sınırları ilerideki Accessibility (a11y) serisinde.
Bir de yazım ayrıntısı var. contrast-color() Nisan 2026'da Baseline oldu; eski tarayıcılar için rozetin color: #ffffff'ı duruyor. Ama yeni kuralı hemen altına yazarsam bir önceki yazıdaki tuzağa düşerim: contrast-color(var(--color-accent)) bir var() içerdiği için parse anında geçerli sayılır ve #ffffff'ı eler; fonksiyonu tanımayan tarayıcıda da computed-value aşamasında geçersiz olur ve metin rengi inherit edilen değere düşer. Bu yüzden kuralı @supports içine aldım:
/* accent ile hangisi daha çok kontrast veriyorsa siyah ya da beyaz; yukarıdaki #ffffff eski tarayıcılar için kalır */
@supports (color: contrast-color(red)) {
.badge {
color: contrast-color(var(--color-accent));
}
}
CSS Matematiği: round(), mod(), rem(), abs() ve sign()
calc(), min(), max() ve clamp()'i serinin önceki yazılarında kullandık. Son iki yılda bunlara yuvarlama ve işaret fonksiyonları eklendi:
/* 4) matematik fonksiyonları */
#round { inline-size: round(up, 0.9rem, 4px); }
#mod { inline-size: mod(-7px, 3px); }
#rem { inline-size: calc(rem(-7px, 3px) + 10px); } /* rem() işareti korur: -1px + 10px */
#abs { inline-size: calc(abs(-12px) * sign(-0.5) + 20px); }
@property --cols { syntax: "<number>"; inherits: false; initial-value: 0; }
.shelf { container-type: inline-size; inline-size: 480px; }
#fit {
/* typed arithmetic: uzunluk / uzunluk birimsiz bir sayıdır */
--cols: round(down, calc((100cqi + 1rem) / 15rem));
/* 480px'in 20rem ile 40rem arasındaki yeri, 0..1 olarak */
opacity: progress(100cqi, 20rem, 40rem);
}
Lab çıktısındaki matematik satırına bak: round(up, 0.9rem, 4px) 14.4px'i 4px'in bir sonraki katına, 16px'e yuvarladı. Bir tasarım sisteminin 4px'lik ızgarasına oturmayan, örneğin rem ya da % ile hesaplanmış bir boşluğu ızgaraya oturtmak için kullanışlı; nearest, down ve to-zero stratejileri de var. mod() ile rem() negatif sayılarda ayrılıyor: mod(-7px, 3px) bölenin işaretini alıyor ve 2px veriyor, rem(-7px, 3px) bölünenin işaretini alıyor ve -1px veriyor (lab'de 10px ekleyip 9px olarak ölçtüm, çünkü genişlik negatif olamaz). Açıları 0–360 aralığına çekmek gibi işlerde mod() doğru seçim. abs() ve sign() ise bir değerin büyüklüğünü ve yönünü ayırıyor: abs(-12px) * sign(-0.5) + 20px 12 × (-1) + 20 = 8px.
progress() ve Typed Arithmetic
Lab'deki .shelf 480px genişliğinde bir container ve #fit iki yeni özelliği birlikte kullanıyor. progress(100cqi, 20rem, 40rem), 480px'in 320 ile 640 piksel arasında nerede durduğunu 0 ile 1 arasında bir sayı olarak veriyor: 0.5. Bu, fluid değerleri clamp()'in içine sıkıştırmadan, açıkça "aralığın neresindeyim" diye yazmayı sağlıyor; sonucu calc() içinde iki değer arasında geçiş yapmak için kullanabilirsin. Eylül 2026'da Baseline oldu.
Typed arithmetic ise calc()'in birimli değerleri birbirine bölebilmesi: (100cqi + 1rem) / 15rem bir uzunluğu bir uzunluğa bölüyor ve sonuç birimsiz bir sayı. Önceden calc() yalnız bir sayıya bölmeye izin veriyordu. Bu örnekte sonuç (480 + 16) / 240 = 2.07; round(down, …) ile 2. Yani 480px'lik rafa, aralarında 1rem boşlukla 14rem'lik kaç kart sığdığını CSS'in kendisi hesaplıyor. Değeri @property ile <number> olarak kaydettiğim --cols'ta okuyabildim; kayıtsız bir property'de hesaplanmamış metin olarak kalırdı. Typed arithmetic, Firefox 158 ile Ekim 2026'da Baseline oldu; Chrome 140'tan beri destekliyordu.
Ufukta: if(), @function, random()
Üç özellik daha var ki CSS'in nasıl yazıldığını değiştirebilir, ama Kasım 2026'da hiçbiri Baseline değil:
/* 5) Baseline değil: yalnız @supports arkasında */
#if-test { inline-size: 10px; }
@supports (width: if(media(width > 1px): 1px; else: 2px)) {
#if-test { inline-size: if(media(width >= 48rem): 200px; else: 100px); }
}
@function --double(--x) { result: calc(var(--x) * 2); }
#function-test { inline-size: 10px; inline-size: --double(21px); }
#random-test { inline-size: 10px; }
@supports (width: random(1px, 2px)) {
#random-test { inline-size: random(100px, 200px, 50px); }
}
if() bir declaration'ın içinde koşula göre değer seçiyor; caniuse'a göre yalnız Chrome 137 ve sonrası destekliyor, Safari ve Firefox desteklemiyor. @function CSS'te kendi fonksiyonunu tanımlamayı sağlıyor; web-features verisinde yalnız Chrome 139 ve sonrası var. random() Safari 26.2 ve Chrome 156 ile geldi, Firefox'ta yalnız Technology Preview'da. Lab çıktısının son satırında üçü de çalışmış görünüyor: if() 200px, @function 42px, random() 100, 150 ya da 200px. Bu sonucu yanlış okuma. Chrome 158 için doğru, ama Playwright'ın WebKit build'i yayınlanmış Safari ile aynı değil ve henüz Safari'de olmayan bazı özellikleri açık getiriyor; if() ve @function'ın orada çalışması Safari'de çalıştıkları anlamına gelmiyor.
Bu yüzden lab'de üçünü de korumalı yazdım. if() ve random() @supports içinde. @function'ın sonucunu kullanan declaration'ın üstüne bir fallback koydum: --double(21px) desteklemeyen tarayıcıda parse anında geçersiz bir değer, bu yüzden declaration atılır ve 10px kalır. Bu durum var()'dan farklı; custom function çağrısı tanınmıyorsa tarayıcı onu daha parse ederken reddediyor. Üretimde bu üçünü ancak sonuç olmadan da sayfanın doğru göründüğü yerlerde, süsleme olarak kullan.
Üretimde Dikkat
Bu yazıdaki özelliklerin yaşları çok farklı. oklch(), oklab() ve color-mix() Mayıs 2023'ten beri Baseline ve Kasım 2025'ten beri widely available. Relative color (Eylül 2024), round()/mod()/rem() (Mayıs 2024) ve abs()/sign() (Haziran 2025) Baseline ama henüz widely değil. contrast-color() (Nisan 2026), progress() ve alpha() (Eylül 2026) ve typed arithmetic (Ekim 2026) ise yeni. Bunları kullandığın her yerde, desteklemeyen tarayıcıda ne göründüğünü düşün: Bookstore'da hover tonu hiç gelmezse buton yine kırmızı kalıyor, contrast-color() yoksa rozet beyaz metinle kalıyor. Hiçbir yerde içerik kaybolmuyor.
Renk değerlerini tasarım dosyasından kopyalarken renk uzayına dikkat et. Tasarım araçları renkleri çoğunlukla hex olarak verir; bunları oklch'ye bir kez çevirip token'ları oklch ile yazmak, sonraki bütün türetmeleri (hover, açık zemin, koyu tema) tutarlı yapar. Yalnız gamut dışına düşen değerlerin farklı ekranlarda farklı görünebileceğini unutma.
Sık Düşülen Tuzaklar
Birincisi, HSL'in lightness'ını algılanan açıklık sanmak. Ölçtüğümüz gibi aynı lightness'taki sarı ile mavi arasında beyaz metin için 8 kattan fazla kontrast farkı var.
İkincisi, color-mix()'te renk uzayını düşünmeden seçmek. in srgb ara renkleri soluklaştırır; hover ve aktif tonları için in oklch daha tutarlı.
Üçüncüsü, contrast-color()'ı erişilebilirlik garantisi saymak. İki seçenekten daha iyisini seçer; orta tonlarda ikisi de yetersiz olabilir.
Dördüncüsü, var() içeren yeni bir fonksiyonu sabit bir fallback'in hemen altına yazmak. Fonksiyonu tanımayan tarayıcıda fallback çoktan elenmiştir; yeni fonksiyonu @supports içine al.
Küçük Bir Deneme
★☆☆ Lab'deki #mix-oklch'i color-mix(in oklch longer hue, var(--cat), yellow) yap. Ton açısının bu kez hangi yoldan döneceğini ve sonucun hangi renge yakın olacağını tahmin et, sonra node scripts/11-color.mjs ile kontrol et.
★★☆ Kategori bağlantılarına, kategori renginden türetilmiş bir alt çizgi ekle: açık temada rengin kendisi, koyu temada oklch(from var(--cat-color) calc(l + 0.2) c h). İki temada da zeminle kontrastını script'e eklediğin bir satırla ölç.
★★★ .book-list için typed arithmetic ile kaç kolon sığdığını hesaplayan bir --cols custom property'si yaz ve bunu kartların başlık boyutunu ayarlamakta kullan: tek kolonda büyük, dört kolonda küçük. Değerleri 360, 768 ve 1280px'te ölç; @property kaydı olmadan ne değiştiğine bak.
Bir sonraki yazıda Sass gibi bir preprocessor'a ihtiyaç duymadan component stili yazmanın iki aracına geçiyoruz: native CSS nesting ve @scope. Bookstore'un kart stillerini iç içe yazacak, kartın içindeki kullanıcı içeriğine stil sızmasını @scope'un donut scope'u ile önleyecek ve Angular'ın emulated encapsulation'ı, ASP.NET Core'un CSS isolation'ı ve Shadow DOM (bir element'e bağlı, dışarıdan stil almayan ayrı element ağacı) ile karşılaştıracağız.