Container Query'ler: Size Query, Style Query ve Component Bazlı Responsive Tasarım
Container query, bir component'in viewport'a değil içine konduğu kutunun genişliğine göre düzen değiştirmesini sağlar; ama container-type subgrid'i kapatır, bu yüzden ikisi aynı element'te kullanılamaz. Bookstore kartını geniş bir bantta yatay, dar bir kolonda dikey gösteriyor; cq birimlerini, Mayıs 2026'da Baseline olan name-only ve style query'leri ve Angular'da host'u container yapmayı Chromium ve WebKit'te ölçüyoruz.
Sürüm notu: Bu yazı CSS Conditional Rules Level 5 ve CSS Containment Level 3 spesifikasyonlarına ve Baseline'ın Kasım 2026 durumuna göre yazıldı; örnekler Chrome 158 ve WebKit 27.2 (Playwright 1.65), Angular 22.2 ve TypeScript 6.0 ile denenmiştir, Firefox desteği web-features 3.46 verisinden alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için katalog kartlarını bir önceki Grid yazısında subgrid'e taşıdık: her kart listeden dört satır ödünç alıyor ve başlıklar, yazarlar, fiyatlar hizalı. Şimdi kartın kendi genişliğine göre düzen değiştirmesini istiyoruz; bunun için kartı bir container yapmak yeterli görünüyor. Lab sayfasında üç kartlık bir raf var ve ortadaki karta tek bir satır ekliyoruz:
.card.measured { container-type: inline-size; }
Bu satır kartın genişliğini, yüksekliğini ya da içeriğini değiştirmiyor; yalnız "bu element'in genişliği sorgulanabilsin" diyor. Ortadaki kartın yazar satırı yine öteki iki kartın yazar satırıyla aynı hizada kalır mı?
Bu yazıda responsive tasarımı sayfadan component'e indiriyoruz. Media query'ler viewport'u sorar; container query'ler ise bir component'in içine konduğu kutuyu. Size query'leri, cq birimlerini, 2026'da Baseline olan name-only ve style query'leri, Angular'da component host'unu container yapmayı ve container query'lerin bedelini ölçeceğiz.
Neden Media Query Yetmiyor?
Bookstore'un katalog sayfasına iki yeni bölüm ekledim: sonuçların üstünde koyu bir "Book of the week" bandı ve soldaki filtre kolonunun altında "Recently viewed". İkisinde de aynı .book-card var:
<section class="featured" aria-labelledby="featured-title">
<h2 id="featured-title">Book of the week</h2>
<article class="book-card in-stock" data-format="hardcover ebook">
<img src="img/dune.svg" alt="Cover of Dune" width="120" height="180">
<h3 class="book-title">Dune</h3>
<span class="badge">Bestseller</span>
<p class="book-author">Frank Herbert</p>
<p class="price">349.00 TRY</p>
</article>
</section>
1280px'lik bir ekranda bu kartlardan biri 992px genişliğinde bir banda, öteki 192px'lik bir kolona düşüyor. Viewport ikisi için de 1280px. @media (width >= 64rem) diye bir kural yazarsan iki kart da aynı kararı alır: ya ikisi de yatay olur ve sol kolondaki kart 192px'e sıkışır, ya da ikisi de dikey kalır ve geniş bant boş kalır. Media query sayfanın genişliğini bilir, kartın nereye konduğunu bilmez. Component'leri yeniden kullandıkça bu sorun büyür: aynı kart katalogda, sepette, kitap detayındaki "benzer kitaplar"da ve yönetim panelinde ayrı genişliklerde durur.
Size Query ve cq Birimleri
Container query iki adımdan oluşur. Önce bir element'i container yaparsın, sonra içindeki element'leri onun boyutuna göre stillersin. Kartların durduğu iki yuvayı container yaptım:
/* tek kartlık yuvalar: içindeki kart viewport'a değil, yuvanın genişliğine uyar */
.featured,
.recently-viewed {
container: book-card / inline-size;
margin-block-end: var(--space);
}
/* koyu bant: içindeki component'ler --surface'i style query ile okur */
.featured {
--surface: dark;
padding: var(--space);
border-radius: var(--radius);
background-color: #1f2937;
color: #f9fafb;
}
/* yalnız isim, size containment yok: kartlar "sidebar'da mıyım?" diye sorabilir */
.filters {
container-name: sidebar;
}
container: book-card / inline-size, container-name: book-card ve container-type: inline-size'ın kısaltması. inline-size, yalnız genişliğin sorgulanabileceğini söylüyor; bunun için tarayıcı element'e inline-size containment uygular, yani genişliğini içeriğinden değil dışarıdan alır. Kart tarafında ise kural şu:
/* geniş yuva: kapak solda, metin sağda */
@container book-card (width >= 32rem) {
.book-card {
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: auto auto 1fr;
grid-template-areas:
"cover title"
"cover author"
"cover price";
column-gap: var(--space);
}
/* 1cqi = yuvanın inline boyutunun %1'i */
.book-card .book-title {
font-size: clamp(1.25rem, 1rem + 1.5cqi, 2rem);
}
}
/* name-only query: sidebar'daki her kartın kapağı daha küçük */
@container sidebar {
.book-card > img {
inline-size: 4rem;
block-size: auto;
}
}
/* style query: yuva koyu olduğunu söyler, kart ona uyar */
@container style(--surface: dark) {
.book-card {
border-color: #4b5563;
background-color: #111827;
color: #f9fafb;
}
.book-card .book-author {
color: #d1d5db;
}
}
@container book-card (width >= 32rem), en yakın book-card isimli atanın genişliğine bakıyor. Bu kurallar .book-card'ın kendisini stilleyebiliyor, çünkü container kartın kendisi değil, onun yuvası. 32rem'den genişse kart iki kolona geçiyor: kapak solda üç satır boyunca, başlık, yazar ve fiyat sağda. cqi birimi de container'ın inline boyutunun yüzde biri; vw'nin component'teki karşılığı. Ölçüm:
node scripts/09-container.mjs
--- chromium
1280px featured w=992 cover 120px, title at x=153 y=17 size 30.88px, columns "120px 822px", background rgb(17, 24, 39)
1280px recently viewed w=192 cover 64px, title at x=17 y=117 size 18.72px, columns "158px", background rgb(255, 255, 255)
1280px list w=244 cover 120px, title at x=17 y=201 size 24px, columns "210px", background rgb(255, 255, 255)
360px featured w=296 cover 120px, title at x=17 y=201 size 18.72px, columns "262px", background rgb(17, 24, 39)
360px recently viewed w=328 cover 64px, title at x=17 y=117 size 18.72px, columns "294px", background rgb(255, 255, 255)
360px list w=328 cover 120px, title at x=17 y=201 size 24px, columns "294px", background rgb(255, 255, 255)
1280px'te aynı sayfada üç farklı kart görünüyor. Banttaki kart 992px genişliğinde ve yatay: kolonlar 120px 822px, başlık kapağın sağında (x=153) ve en üstte (y=17). Başlığın boyutu 30.88px; bandın içerik genişliği 992px olduğu için 1cqi = 9.92px ve 1rem + 1.5cqi = 16 + 14.88. Sol kolondaki kart 192px ve dikey; listedeki kart da dikey ve subgrid'li. 360px'te üç kart da dikey, çünkü bant artık 296px. WebKit aynı değerleri verdi; yalnız font boyutlarını 30.879999px gibi daha uzun ondalıkla yazıyor.
Listedeki kartlar bu kurallardan hiç etkilenmiyor, çünkü onların atalarında book-card isimli bir container yok. Sorgu eşleşecek bir container bulamayınca yanlış sayılır ve kart varsayılan, dikey düzende kalır. Bunu bilerek yaptım; sebebi bir sonraki bölümde.
Subgrid ile Container Query Aynı Element'te Çalışmaz
Tahmin sorusunun lab sayfası şöyle:
/* 1) subgrid kartlar; ikincisi aynı zamanda bir size container */
.shelf { display: grid; grid-template-columns: repeat(3, 180px); gap: 16px; }
.card { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 4px; }
.card.measured { container-type: inline-size; }
/* 2) bir container kendi query'siyle kendini stilleyemez */
.panel { container-type: inline-size; inline-size: 400px; }
@container (width >= 300px) {
.panel { outline: 2px solid; }
.panel p { font-weight: 700; }
}
/* 3) iki eksende size containment: yükseklik artık içerikten gelmez */
.both { container-type: size; }
--- chromium
Middlemarch author y=69 rows "subgrid [] [] [] []"
Dune author y=40 rows "none"
Emma author y=69 rows "subgrid [] [] [] []"
panel: outline none, text weight 700
container-type: size, height 0px
Ortadaki Dune kartının yazar satırı 40. pikselde, öteki iki kartınki 69. pikselde. container-type: inline-size eklemek subgrid'i kapattı ve Chromium grid-template-rows'un kullanılan değerini none olarak raporluyor. WebKit'in çıktısı ilginç:
--- webkit
Middlemarch author y=69 rows "subgrid [] [] [] []"
Dune author y=40 rows "subgrid"
Emma author y=69 rows "subgrid [] [] [] []"
WebKit computed değerde hâlâ subgrid yazıyor ama yerleşim aynı: Dune'un yazarı 40. pikselde. Bu bir tarayıcı hatası değil, spesifikasyonun gereği. CSS Conditional Rules Level 5'e göre container-type: inline-size element'e style ve inline-size containment uygular ve bağımsız bir formatting context oluşturur. CSS Grid Level 2 de bağımsız formatting context oluşturmak zorunda kalan bir grid'in (örnek olarak layout containment ve absolute positioning sayılıyor) subgrid olamayacağını söylüyor. Kısacası bir element ya ebeveyninin satırlarına katılır ya da kendi boyutunu dış dünyadan bağımsız sorgulatır; ikisini birden yapamaz.
Bookstore'da bu yüzden işleri böldüm. Katalog listesinde hizalama önemli ve kartlar zaten 14rem ile 28rem arasında, yani listede yatay düzene hiç ihtiyaç yok: orada subgrid var. Tek kartlık yuvalarda (bant, sol kolon) hizalanacak komşu yok: orada container query var. Bu tür bir çakışmayı tasarım aşamasında görmek, iki tekniği aynı karta uygulayıp nedenini sonradan aramaktan çok daha ucuz.
Container Kendini Sorgulayamaz, Size Containment Yüksekliği Çökertebilir
Çıktının son iki satırı iki tuzağı daha gösteriyor. .panel 400px genişliğinde bir container ve @container (width >= 300px) içinde hem kendisine hem içindeki paragrafa kural yazılmış. Paragraf kalınlaştı (font-weight 700), panelin kendi outline'ı ise none. Bir container query, element'in kendisini değil, en yakın container atasını sorar; panelin atalarında container olmadığı için panelin kendi kuralı hiç eşleşmedi. Bir component'in kendi kutusunu da değiştirmek istiyorsan ya onu bir yuvaya koyarsın (Bookstore'daki .featured gibi) ya da component'in içine bir sarmalayıcı element eklersin.
İkinci tuzak container-type: size. Bu değer iki eksende de containment uygular: element'in yüksekliği de artık içeriğinden gelmez. Lab'deki .both'un içinde bir paragraf var ama yüksekliği 0px; paragraf kutudan taşıyor. size'ı yalnız yüksekliği zaten dışarıdan belli olan element'lerde (sabit yükseklikli bir panel, grid hücresi) kullan. Yatay düzen değişiklikleri için neredeyse her zaman inline-size yeterli.
Name-only ve Style Query'ler
2026'da iki yeni container query türü Baseline oldu. Name-only query, bir koşul olmadan yalnız ismi sorar: @container sidebar { … } "bir sidebar isimli container'ın içindeysem" demek. Bookstore'da filtre kolonuna yalnız container-name: sidebar verdim, container-type vermedim. Bu önemli: name-only ve style query'ler boyut sormadığı için containment gerektirmez, yani subgrid'i ya da yükseklik hesabını bozmaz. Sol kolondaki her kart küçük kapakla görünüyor; çıktıda cover 64px. Name-only query'ler Mayıs 2026'da Baseline oldu.
Style query ise container'ın bir custom property değerini sorar. Koyu bant --surface: dark diyor; kart da @container style(--surface: dark) ile kendi zeminini ve metin rengini koyulaştırıyor. Çıktıdaki background rgb(17, 24, 39) bu kuralın sonucu. Aynı şeyi .featured .book-card selector'üyle de yazabilirdik; style query'nin farkı, kartın bandın class adını bilmek zorunda olmaması. Bandın, bir dialog'un ya da koyu bir footer'ın yalnız --surface: dark demesi yeterli; kart "hangi koyu yerdeyim" diye sormaz, "zemin koyu mu" diye sorar. Style query'ler Firefox 151 ile Mayıs 2026'da Baseline oldu; Chrome 111'den, Safari 18'den beri destekliyordu. Custom property'lerin kendisini ve theming'i serinin bir sonraki yazısında ayrıntılı işleyeceğiz.
Angular'da Component Host'unu Container Yapmak
Angular'da her component'in bir host element'i var (<app-book-card>). Bu host varsayılan olarak display: inline ve container query'ler için doğal bir yuva. Projenin angular/ klasörüne küçük bir bookstore-web projesi kurdum (Angular 22.2 projeleri TypeScript 6.0 istediği için TypeScript 6.0 ile):
import { Component, input } from '@angular/core';
@Component({
selector: 'app-book-card',
template: `
<article class="card">
<img [src]="cover()" [alt]="'Cover of ' + bookTitle()" width="120" height="180">
<h3>{{ bookTitle() }}</h3>
<p class="author">{{ author() }}</p>
</article>
`,
styles: `
/* host element container'dır: her app-book-card kendi yuvasını ölçer */
:host {
display: block;
container: book-card / inline-size;
}
.card {
display: grid;
gap: 0.25rem 1rem;
}
/* container kendini sorgulayamaz, bu yüzden düzen içerideki .card'da */
@container book-card (width >= 32rem) {
.card {
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: auto 1fr;
}
img {
grid-row: span 2;
}
}
`,
})
export class BookCard {
readonly bookTitle = input.required<string>();
readonly author = input.required<string>();
readonly cover = input.required<string>();
}
:host kuralı host'u container yapıyor; düzen ise içerideki .card'da, çünkü az önce gördüğümüz gibi host kendini sorgulayamaz. Uygulama aynı component'i 14rem'lik bir kolona ve geniş bir ana alana koyuyor. Build'den sonra Playwright çıktıyı küçük bir yönlendirme ile sunuyor, ayrıca bir web sunucusu gerekmiyor:
cd angular
npm install
npx ng build
cd ..
node scripts/09-angular.mjs
--- chromium
Emma host w=224 container-type inline-size, .card columns "224px"
Dune host w=768 container-type inline-size, .card columns "120px 632px"
[_nghost-ng-c207890517] { display: block; container: book-card / inline-size; }
@container book-card (width >= 32rem) { .card[_ngcontent-ng-c207890517] { grid-template-columns: auto minmax(0px, 1fr); grid-template-rows: auto 1fr; } img[_ngcontent-ng-c207890517] { grid-row: span 2; } }
--- webkit
Emma host w=224 container-type inline-size, .card columns "224px"
Dune host w=768 container-type inline-size, .card columns "120px 632px"
[_nghost-ng-c207890517] { display: block; container-name: book-card; container-type: inline-size; }
@container book-card (width >= 32rem) { .card[_ngcontent-ng-c207890517] { grid-template-columns: auto minmax(0px, 1fr); grid-template-rows: auto 1fr; } img[_ngcontent-ng-c207890517] { grid-row-start: span 2; grid-row-end: auto; } }
Kolondaki Emma 224px ve tek kolonlu, ana alandaki Dune 768px ve 120px 632px. Son iki satır Angular'ın emulated encapsulation'ının yaptığı dönüşümü gösteriyor: :host, [_nghost-ng-c207890517] attribute selector'üne çevrilmiş, @container içindeki selector'lere de [_ngcontent-…] eklenmiş; container query olduğu gibi kalmış. WebKit aynı kuralı container-name ve container-type longhand'leriyle yazıyor. Angular'ın stil kapsülleme modlarını serinin nesting ve @scope yazısında karşılaştıracağız.
Container query'den önce bu iş JavaScript'le yapılırdı: component kendi genişliğini ResizeObserver ile izler ve bir class ya da signal değiştirirdi. JavaScript serisinin İleri bölümündeki Resize Observer yazısı bu yöntemi anlatıyor. Düzen değişikliği için container query daha iyi: JavaScript çalışmadan, ilk render'da doğru düzen gelir ve layout ile stil aynı adımda hesaplanır. ResizeObserver ise genişliğe göre farklı veri yüklemek ya da farklı bir component göstermek gibi CSS'in yapamadığı işlerde anlamlı.
Üretimde Dikkat
Size query'ler ve cq birimleri Şubat 2023'te Baseline oldu ve Ağustos 2025'ten beri widely available. Name-only query'ler (Mayıs 2026) ve style query'ler (Mayıs 2026) ise newly available: son iki tarayıcı sürümünü kapsayan bir hedefin varsa doğrudan kullanabilirsin; daha eski tarayıcılara da hizmet veriyorsan bu kuralları ek görünüm olarak düşün. Desteklemeyen tarayıcı @container bloğunu yok sayar ve kart varsayılan düzende kalır; Bookstore'da varsayılan düzen dikey kart olduğu için içerik her durumda okunur.
Her container bir containment sınırı demek: tarayıcı container'ın genişliğini içeriğe bakmadan hesaplar. Bu performans açısından iyi ama bir kutunun genişliği içeriğine bağlıysa (width: fit-content, bir flex item'ın flex-basis: auto'su) container yapınca o kutu daralabilir. Container'ı her zaman genişliği dışarıdan belli olan bir yuvaya koy: grid hücresi, block element, ya da Angular'da display: block verilmiş host.
Sık Düşülen Tuzaklar
Birincisi, subgrid'li bir karta container-type vermek. Ölçtüğümüz gibi subgrid sessizce kapanır; hiçbir uyarı çıkmaz, yalnız hizalama bozulur.
İkincisi, container'ın kendini sorgulayabileceğini sanmak. Kural en yakın container atasına bakar; component'in kendi kutusunu değiştirmek için yuvaya ya da içeride bir sarmalayıcıya ihtiyacın var.
Üçüncüsü, düşünmeden container-type: size kullanmak. Yüksekliği içerikten gelen her element 0px'e çöker.
Dördüncüsü, media query'leri tamamen kaldırmaya çalışmak. Sayfa iskeleti viewport'a bağlı bir karar; filtre kolonunun yan tarafa geçmesi ekranın genişliğine göre olmalı. Media query sayfa düzeni için, container query component düzeni için.
Küçük Bir Deneme
★☆☆ lab/09-pitfalls.html'de .card.measured'ın container-type: inline-size'ını container-name: measured ile değiştir. Dune'un yazar satırının nereye geleceğini tahmin et, sonra node scripts/09-container.mjs ile kontrol et.
★★☆ book.html'e, kitap özetinin altına "Similar books" başlıklı bir bölüm ekle ve içine iki .book-card koy. Bölümü book-card container'ı yap ve kartların 360px'te dikey, 1280px'te yatay göründüğünü script'le doğrula.
★★★ Angular projesindeki BookCard'a variant isimli bir input ekle ve host'a --variant custom property'si olarak yaz (host: { '[style.--variant]': 'variant()' }). Kartın içindeki stilleri @container style(--variant: compact) ile değiştir ve iki farklı variant'ı Playwright'la ölç.
Bir sonraki yazıda bu yazıda kullanmaya başladığımız custom property'lere odaklanıyoruz: cascade ve inheritance içinde nasıl yaşadıkları, @property ile tip kazanmaları, "invalid at computed-value time" tuzağı ve light-dark() ile Bookstore'a koyu tema eklemek.