filter, backdrop-filter ve Blend Mode'lar: Görselleri ve Arka Planı CSS ile İşlemek
filter element'i bütün çocuklarıyla tek bir resim gibi soldan sağa function function işler, backdrop-filter ise arkasında kalanı; blend mode'lar katmanların renklerini birleştirir ve isolation: isolate bu birleşmeyi bileşenle sınırlar. Yazı drop-shadow ile box-shadow farkını, Bookstore'da stokta olmayan kapağı, yedek arka planlı buzlu cam filtre çubuğunu ve background-blend-mode ile duotone banner'ı ölçümlerle anlatır.
Sürüm notu: Bu yazı Filter Effects Module Level 1 ve Level 2 ile Compositing and Blending Level 1 spesifikasyonlarına ve Baseline'ın Kasım 2026 durumuna (web-features 3.46.0) göre yazıldı; örnekler Chromium 158 ve WebKit 27.2 (Playwright 1.65) ile denenmiştir, Firefox desteği web-features ve MDN'in (Mozilla'nın web dokümantasyon projesi) browser-compat-data 8.1.9 verisinden alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) kataloğunda stokta kalmayan kitapları soluk göstermek, kataloğun üstüne kaydırınca da okunur kalan bir filtre çubuğu koymak ve ana sayfadaki kampanya banner'ını iki renkli bir görünüme çevirmek istiyoruz. Hepsi görseli ya da arkasındakini CSS ile işlemek demek. Önce küçük bir deney: kırmızı zeminli bir kareye iki filter'ı iki farklı sırayla verdik.
/* 1) filter function'ları soldan sağa uygulanır */
#gray-sepia { filter: grayscale(1) sepia(1); }
#sepia-gray { filter: sepia(1) grayscale(1); }
Kırmızı köşe iki karede de aynı renge mi döner? Biri gri, biri kahverengimsi olacaksa hangisi hangisi? Cevabını not et.
filter Function'ları ve Sıraları
filter property'si boşlukla ayrılmış function'lar alır: grayscale(), sepia(), saturate(), hue-rotate(), invert(), brightness(), contrast(), opacity(), blur() ve drop-shadow(). Oranlar sayı ya da yüzde olarak verilir; grayscale(1) ile grayscale(100%) aynıdır. Function'lar soldan sağa, her biri bir öncekinin çıktısına uygulanır:
node scripts/07-filters.mjs
--- chromium
original filter none red corner: rgb(220, 38, 38)
gray-sepia filter grayscale(1) sepia(1) red corner: rgb(104, 92, 72)
sepia-gray filter sepia(1) grayscale(1) red corner: rgb(110, 110, 110)
--- webkit
original filter none red corner: rgb(220, 38, 38)
gray-sepia filter grayscale(1) sepia(1) red corner: rgb(102, 91, 70)
sepia-gray filter sepia(1) grayscale(1) red corner: rgb(108, 108, 108)
Sıra sonucu değiştiriyor. grayscale(1) sepia(1) önce rengi griye çeviriyor, sonra sepia tonu ekliyor: köşe kahverengimsi (104, 92, 72). sepia(1) grayscale(1) ise önce sepia veriyor, sonra bütün rengi siliyor: köşe düz gri (110, 110, 110). Bir önceki yazıdaki transform function'ları gibi düşün: liste bir pipeline, son adım sonucu belirler. İki motorun sayıları bir-iki birim farklı; renk dönüşümünün yuvarlaması motordan motora değişebiliyor, bu yüzden testlerde tam eşitlik yerine bir tolerans kullan.
Hazır function'lar yetmezse filter: url(#id) ile sayfadaki bir SVG (vektörel görsel formatı) <filter> element'ine bağlanabilirsin; renk matrisleri ve gürültü gibi efektler oradan gelir ve Baseline'da çok eskiden beri var. Bir görsele filter uygulayıp sonucu background-image olarak kullanan filter() function'ı ise yalnız Safari'de var ve Baseline değil.
drop-shadow() ile box-shadow Farkı
Önceki yazıda box-shadow'u kutunun gölgesi olarak kullandık. Arka planı şeffaf bir görselde, örneğin bir PNG (kayıpsız sıkıştırılmış görsel formatı) ya da SVG dosyasında, gölgenin kutuya değil şeklin kendisine düşmesi gerekiyorsa filter: drop-shadow() kullanılır. Farkı görmek için, köşesine yaslanmış bir kitap görselini (şeffaf PNG) iki yöntemle gölgelendirdim ve kırmızı gölge piksellerini saydım:
/* 2) box-shadow kutuyu, drop-shadow pikselleri takip eder */
.tilted { display: block; inline-size: 180px; block-size: 180px; margin-block-end: 20px; }
#box-shadow { box-shadow: 10px 10px 0 #b91c1c; }
#drop-shadow { filter: drop-shadow(10px 10px 0 #b91c1c); }
box-shadow red shadow pixels inside the box: 0 outside: 3500
drop-shadow red shadow pixels inside the box: 1964 outside: 20
box-shadow'un 3500 pikselinin hepsi 180×180'lik kutunun dışında: gölge kutunun sağına ve altına düşen bir L şekli. Kutunun içindeki şeffaf alanda hiç gölge yok, çünkü dış gölge yalnız border box'ın dışına çizilir. drop-shadow() ise 1964 pikselin neredeyse hepsini kutunun içine, eğik kitabın kenarlarının hemen yanına çiziyor; görselin opak pikselleri neredeyse kutunun kenarına kadar geldiği için yalnız 20 piksel dışarı taşıyor. İki farkı daha bil: drop-shadow()'un spread ve inset parametresi yok, ve filter olduğu için element'in içindeki her şeyin (metin dahil) birleşik şekline göre çizilir.
Stokta Olmayan Kitap: filter Nereye Verilir?
Stokta olmayan kitabın kapağı gri ve soluk görünsün, üstünde de kırmızı "Sold out" etiketi dursun. İlk akla gelen, filter'ı bütün karta vermek:
/* 3) karttaki filter etiketini de griye çevirir */
.lab-card { display: inline-block; padding: 8px; background: #ffffff; }
.lab-card .badge { display: block; padding: 4px 8px; background: #b91c1c; color: #ffffff; }
#card-filter .lab-card { filter: grayscale(1); }
#img-filter .lab-card img { filter: grayscale(1); }
card-filter badge: rgb(61, 61, 61) cover: rgb(77, 77, 77)
img-filter badge: rgb(185, 28, 28) cover: rgb(77, 77, 77)
Filter karta verilince etiket de griye dönüyor: arka planı 185, 28, 28 kırmızısından 61, 61, 61 griye inmiş. filter element'in tamamını, bütün çocuklarıyla birlikte tek bir resim gibi işler; dışarıda bırakmak istediğin bir çocuğu kurtarmanın yolu yok. Filter'ı yalnız görsele verdiğimizde kapak aynı griye dönüyor, etiket kırmızı kalıyor. Bookstore'da da böyle yaptık:
<li class="book-card is-sold-out">
<span class="badge">Sold out</span>
<div class="cover"><img src="img/cover-hobbit.webp" alt="" width="240" height="360"></div>
/* stokta yok: kapak solar, etiket rengini korur */
.book-card.is-sold-out .cover img {
filter: grayscale(1);
opacity: 0.6;
}
node scripts/07-pages.mjs
--- chromium
sold out cover: filter grayscale(1), opacity 0.6
sold out badge pixel: rgb(185, 28, 28)
filter bar after scroll: y=0, backdrop-filter blur(12px), background rgba(249, 250, 251, 0.7)
filter bar pixel over the cover: rgb(181, 203, 194)
banner without duotone red-dominant pixels in the top 60px: 10812 (normal, normal)
banner with duotone red-dominant pixels in the top 60px: 0 (normal, lighten, multiply, luminosity)
--- webkit
sold out cover: filter grayscale(1), opacity 0.6
sold out badge pixel: rgb(185, 28, 28)
filter bar after scroll: y=0, backdrop-filter blur(12px), background rgba(249, 250, 251, 0.7)
banner without duotone red-dominant pixels in the top 60px: 10766 (normal)
banner with duotone red-dominant pixels in the top 60px: 0 (normal, lighten, multiply, luminosity)
--- chromium, forced colors
banner: background-image none, none, none, none, background-color rgb(255, 255, 255), color rgb(0, 0, 0)
İlk iki satır stokta olmayan kart için: kapakta grayscale(1) ve 0.6 opacity, etiket pikseli hâlâ kırmızı (185, 28, 28). Burada iki şeye dikkat et. Birincisi, durumu yalnız renkle anlatmıyoruz; "Sold out" metni görünür ve screen reader tarafından okunur. İkincisi, filter görünüşü değiştirir, davranışı değiştirmez. Bu kartta bir "Add to cart" butonu olsaydı, kapağı griye çevirmek butonu devre dışı bırakmazdı; butona disabled attribute'u vermek gerekirdi.
filter'ın none dışındaki her değeri, transform gibi, element'i yeni bir stacking context yapar ve içindeki position: fixed element'ler için containing block olur. Ayrıntı ve ölçüm sitedeki CSS serisinin (CSS3: Flexbox, Grid ve Modern CSS) 04. yazısında; filter'ı küçük bir element'e (burada img'ye) vermek bu yan etkileri de dar tutar.
backdrop-filter ile Buzlu Cam Filtre Çubuğu
filter element'in kendisini işler; backdrop-filter ise element'in arkasında kalanı. Yarı saydam bir arka planla birlikte kullanınca arkadaki içeriği bulanıklaştıran "buzlu cam" efekti çıkar. Ne kadar fark ettiğini görmek için 4px'lik siyah-beyaz çizgilerin üstüne yarı saydam beyaz bir çubuk koydum ve çubuğun ortasından 16 piksellik bir satır okudum:
/* 4) 4px'lik çizgilerin üstünde buzlu cam çubuk */
.stripes { position: relative; inline-size: 200px; block-size: 100px;
background: repeating-linear-gradient(90deg, #000000 0 4px, #ffffff 4px 8px); }
.bar { position: absolute; inset: 30px 0 auto; block-size: 40px; background: rgb(255 255 255 / 0.5); }
#frosted .bar { backdrop-filter: blur(8px); }
plain backdrop-filter none row min 128, max 255
frosted backdrop-filter blur(8px) row min 182, max 201
Filter'sız çubukta değerler 128 ile 255 arasında gidip geliyor: çizgiler çubuğun altından olduğu gibi görünüyor ve üstteki metin bu zeminde zor okunur. blur(8px) ile değerler 182 ile 201 arasına sıkışıyor; arka plan neredeyse düz bir griye dönüyor. Bu ölçümü yalnız Chromium'da gösteriyorum: Playwright'ın WebKit build'i ekran görüntüsünde backdrop-filter'ı çizmedi, computed değer ise iki motorda da blur(8px).
backdrop-filter Eylül 2024'te Safari 18'in prefix'siz desteğiyle Baseline newly available oldu; widely available olmasına daha zaman var. Safari 9'dan 17'ye kadar -webkit-backdrop-filter olarak çalışıyor. Bu yüzden Bookstore'un katalog sayfasındaki sticky filtre çubuğunu iki katmanlı yazdık: varsayılan arka plan tek başına okunur olacak kadar opak, bulanıklık destekleniyorsa arka plan biraz daha saydamlaşıyor:
.filter-bar {
position: sticky;
inset-block-start: 0;
z-index: 5;
display: flex;
gap: 0.5rem;
padding-block: 0.5rem;
/* tek başına okunur: backdrop-filter'sız tarayıcılar bunu gösterir */
background: rgb(249 250 251 / 0.95);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.filter-bar {
background: rgb(249 250 251 / 0.7);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}
}
.filter-bar button {
padding: 0.25rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-surface);
font: inherit;
}
.filter-bar button[aria-pressed="true"] { border-color: var(--color-link); color: var(--color-link); }
Sayfa ölçümünün üçüncü satırı: 300px kaydırdıktan sonra çubuk hâlâ y=0'da, iki motorda da blur(12px) ve 0.7 saydamlıkta arka plan uygulanmış. Chromium'da çubuğun yeşil Middlemarch kapağının üstüne gelen pikseli (181, 203, 194): kapağın rengi açık bir tonla seziliyor ama kapaktaki yazı ve çizgiler görünmüyor. @supports koşulunda iki yazımı birden kontrol etmek önemli; yalnız backdrop-filter'ı kontrol etseydik Safari 17 kullanıcısı prefix'li desteğe rağmen opak arka planı görürdü (bu da bir sorun değil, yalnız daha az şık). Sticky konumlandırma ve z-index değerinin kendisi CSS3 serisinin 04. yazısında.
Blend Mode'lar: mix-blend-mode ve isolation
Blend mode, bir katmanın renginin arkasındaki renkle nasıl birleşeceğini belirler: multiply renkleri çarpar ve koyulaştırır (beyaz etkisizdir), screen tersini yapar ve açar (siyah etkisizdir), overlay ikisinin karışımıdır; lighten ve darken her kanalda büyük ya da küçük değeri seçer, luminosity üstteki katmanın parlaklığını alttakinin rengiyle birleştirir. mix-blend-mode bir element'i arkasındaki her şeyle birleştirir. "Arkasındaki her şey" kısmı önemli:
<section class="page" id="blended"><div class="group"><div class="chip"></div></div></section>
<section class="page" id="isolated"><div class="group"><div class="chip"></div></div></section>
/* 6) mix-blend-mode arkasındaki her şeyle birleşir, isolation bunu sınırlar */
.page { padding: 20px; background: #f59e0b; }
.chip { inline-size: 60px; block-size: 30px; background: #1d4ed8; mix-blend-mode: screen; }
#isolated .group { isolation: isolate; }
blended chip: rgb(246, 188, 218)
isolated chip: rgb(29, 78, 216)
Mavi çip (29, 78, 216) screen ile turuncu sayfanın rengiyle birleşip pembemsi bir açık renge (246, 188, 218) dönüyor; araya giren şeffaf .group bunu engellemiyor. .group'a isolation: isolate verince grup yeni bir stacking context oluyor ve çip yalnız grubun içindekilerle birleşiyor. Grubun içi boş olduğu için çip kendi rengini koruyor. Bir component'in içindeki blend efektinin sayfanın arka planına sızmasını istemiyorsan component'in kök element'ine isolation: isolate ver.
Kampanya Banner'ında Duotone
Duotone, görselin karanlık tonlarını bir renge, açık tonlarını başka bir renge eşleyen iki renkli bir görünüm. background-blend-mode, element'in background katmanlarını birbirleriyle birleştirir ve element'in metnine dokunmaz. Duotone'u üç katmanla kurduk; mantığı görmek için görsel yerine siyahtan beyaza giden bir gradient kullandım:
/* 5) background katmanlarıyla duotone: koyu → lacivert, açık → açık amber */
#duotone { inline-size: 200px; block-size: 40px; background-color: #ffffff;
background-image: linear-gradient(#1e3a8a, #1e3a8a), linear-gradient(#fde68a, #fde68a),
linear-gradient(to right, #000000, #ffffff);
background-blend-mode: lighten, multiply, luminosity; }
--- chromium
duotone left rgb(30, 58, 138) middle rgb(127, 116, 138) right rgb(251, 228, 138)
--- webkit
duotone left rgb(30, 58, 138) middle rgb(128, 116, 138) right rgb(252, 229, 138)
Katmanlar alttan üste birleşir. En alttaki gradient, beyaz zemine luminosity ile oturur ve gri tonlara döner (zeminin rengi olmadığı için yalnız parlaklık kalır). Üstündeki açık amber katman multiply ile bu griyi çarpar: siyah siyah kalır, beyaz açık ambere döner. En üstteki lacivert katman lighten ile her kanalda büyük olanı seçer: siyah laciverte (30, 58, 138) döner, açık amber olduğu gibi kalır. Sonuç: soldan sağa lacivert, ortada (127, 116, 138) bir ara ton, sağda açık amber (251, 228, 138). Kural şu: açık rengin her kanalı koyu renginkinden büyük ya da eşit olmalı; yoksa lighten o kanalda koyu rengi seçer. İlk denemede turuncuyu (#f59e0b) seçtim ve sağ uç (243, 157, 138) çıktı, çünkü turuncunun mavi kanalı (11) lacivertinkinden (138) küçük.
Ana sayfadaki banner'da aynı üç katmanın üstünde, metni okunur tutan karartma gradient'i normal blend ile duruyor:
.promo-banner {
display: grid;
align-content: end;
min-block-size: 16rem;
padding: 2rem;
border-radius: 0.75rem;
color: #ffffff;
/* görsel gelene kadar ve hiç gelmezse görünür */
background-color: #1e293b;
/* ilk katman en üstte: karartma metni okunur tutar */
background-image:
linear-gradient(to top, rgb(15 23 42 / 0.9), rgb(15 23 42 / 0.3) 70%),
linear-gradient(#1e3a8a, #1e3a8a),
linear-gradient(#fde68a, #fde68a),
image-set(
url("../../img/banner-1x.avif") 1x type("image/avif"),
url("../../img/banner-2x.avif") 2x type("image/avif"),
url("../../img/banner-1x.webp") 1x type("image/webp"),
url("../../img/banner-2x.webp") 2x type("image/webp")
);
/* duotone: fotoğraf griye döner (luminosity), açık kısımlar → açık amber (multiply),
koyu kısımlar → lacivert (lighten); karartma normal çizilir */
background-blend-mode: normal, lighten, multiply, luminosity;
background-size: cover;
background-position: center;
}
Sayfa ölçümündeki banner satırları: bu yazıdan önceki banner'ın (karartma ve görsel) üst 60 pikselinde kırmızı ağırlıklı 10812 piksel vardı; duotone'la bu sayı 0. Kitap sırtlarındaki kırmızı, mavi ve yeşilin hepsi lacivert ile amber arasındaki tonlara dönüyor. Metin beyaz kalıyor, çünkü blend yalnız background katmanları arasında. Görsel daha yüklenmeden ya da hiç yüklenmezse koyu background-color ve renk katmanları yine okunur bir zemin bırakıyor.
Üretimde Dikkat
blur() ve backdrop-filter pahalı efektler: maliyet bulanıklık yarıçapı ve alanla birlikte artar. backdrop-filter'ın bir ek maliyeti var: arkasındaki içerik her değiştiğinde (sayfa kaydığında, arkada bir animasyon oynadığında) bulanıklık yeniden hesaplanır. Ekranın bir şeridini kaplayan bir çubuk için bu kabul edilebilir; sayfanın tamamını kaplayan bir overlay'e büyük bir blur vermek ise düşük güçlü cihazlarda kaydırmayı takıltılı yapabilir. Efekti küçük alanlarda ve küçük yarıçaplarla tut, gerçek bir telefonda DevTools'un performance panelinde dene.
Filter'ı animasyonla değiştirmek (ör. hover'da grayscale(1)'den grayscale(0)'a) mümkün; iki tarafta da aynı function listesi olursa tarayıcı değerleri tek tek ara değere taşır. Listeler farklıysa sonuç tahmin ettiğin gibi olmayabilir. Animasyonun genel kuralları CSS3 serisinin 13. yazısında.
Sayfa ölçümünün son satırı forced colors modunu (Windows'un yüksek contrast teması gibi) gösteriyor: Chromium bu modda banner'ın dört background katmanının dördünü de none yapıyor, arka planı beyaza ve metni siyaha çeviriyor. Duotone tamamen kayboluyor, ama banner'daki bilgi metinde olduğu için bir şey kaybolmuyor. Bilgiyi yalnız bir filter ya da blend efektiyle taşıyan bir tasarım (ör. yalnız gri kapakla "stokta yok" demek) ise bu modda ve renk ayırt etmekte zorlanan kullanıcılar için bilgiyi kaybeder; "Sold out" metninin görünür olması bu yüzden önemli.
Sık Düşülen Tuzaklar
Birincisi, filter'ı bir kartın tamamına verip içindeki etiketin, butonun ya da ikonun da griye döndüğünü sonradan fark etmek. Filter'ı değişmesi gereken en küçük element'e ver.
İkincisi, filter ile "devre dışı" görünümü verip element'i gerçekten devre dışı bırakmamak. Gri bir buton hâlâ tıklanır ve klavyeyle odaklanır; disabled attribute'u ya da uygun ARIA (Accessible Rich Internet Applications; erişilebilirlik için HTML attribute'ları) durumu gerekir.
Üçüncüsü, backdrop-filter için yedek arka plan yazmamak. Desteklemeyen tarayıcıda yarı saydam çubuğun altından metin ve görsel olduğu gibi görünür ve çubuktaki metin okunmaz.
Dördüncüsü, filter ve backdrop-filter'ın stacking context ve containing block oluşturduğunu unutmak. İçinde position: fixed bir element ya da dışarıya taşması gereken bir dropdown varsa davranışı değişir.
Beşincisi, mix-blend-mode'un etkisini component'le sınırlı sanmak. Element, kendisine en yakın stacking context'e kadar arkasındaki her şeyle birleşir; sınır istiyorsan isolation: isolate yaz.
Baseline Durumu
node scripts/baseline.mjs filter backdrop-filter mix-blend-mode background-blend-mode isolation svg-filters filter-function
filter widely 2019-03-07 Chrome 53, Firefox 35, Safari 9.1, iOS 9.3
backdrop-filter newly 2024-09-16 Chrome 76, Firefox 103, Safari 18, iOS 18
mix-blend-mode widely 2022-07-15 Chrome 41, Firefox 32, Safari 8, iOS 8
background-blend-mode widely 2022-07-15 Chrome 35, Firefox 30, Safari 8, iOS 8
isolation widely 2022-07-15 Chrome 41, Firefox 36, Safari 8, iOS 8
svg-filters widely 2018-01-29 Chrome 5, Firefox 3, Safari 6, iOS 6
filter-function not Baseline Chrome -, Firefox -, Safari 9.1, iOS 9.3
| Özellik | Durum | Not |
|---|---|---|
filter (function'lar, drop-shadow()) | Widely available | Function'lar soldan sağa uygulanır |
backdrop-filter | Newly available (2024-09) | Safari 9–17'de -webkit- prefix'i ile |
mix-blend-mode, background-blend-mode | Widely available | plus-darker değeri yalnız Safari'de |
isolation | Widely available | Blend'i component'le sınırlar |
SVG filter'ları (url(#id)) | Widely available | Renk matrisi, gürültü gibi özel efektler |
filter() function'ı | Baseline değil | Yalnız Safari |
Küçük Bir Deneme
★☆☆ Lab'deki kareye brightness(1.5) contrast(2) ve contrast(2) brightness(1.5) ver; 07-filters.mjs'e iki satır ekleyip kırmızı köşenin iki sırada hangi renge döndüğünü ölç.
★★☆ Stokta olmayan kartın kapağı hover'da renklensin (grayscale(0), opacity: 1) ve geçiş prefers-reduced-motion'a uysun. Hover öncesi ve sonrası kapak pikselini 07-pages.mjs ile ölç.
★★★ Duotone'u lacivert ve amber yerine Bookstore'un vurgu kırmızısı (#b91c1c) ile açık bir krem rengiyle kur. Önce kanal kuralına uyan bir krem seç, sonra lab'deki gradient ile sol, orta ve sağ uçların beklediğin renklere düştüğünü doğrula.
Bir sonraki yazıda element'leri kırpıyor ve maskeliyoruz: clip-path'in basic shape'leri ve shape(), gradient ve SVG ile mask-image, kitap detayında kapağın çevresinden akan metin için shape-outside.