CSS Grid 2: Subgrid, Alignment ve "Grid mi Flexbox mı?" Kararı
Subgrid, bir kartın satırlarını kendi hesaplamak yerine listenin satırlarından ödünç almasını sağlar; böylece farklı uzunlukta başlıkları olan kartlarda başlık, yazar ve fiyat satırları aynı hizaya gelir. Bookstore kataloğunu subgrid'e taşıyor, justify/align ve place-* property'lerini ölçüyor, Grid ile Flexbox arasındaki seçimi içerikten dışa ve layout'tan içe kuralıyla yapıyor, masonry için grid-lanes'in Kasım 2026 durumuna bakıyoruz.
Sürüm notu: Bu yazı CSS Grid Layout Level 2 spesifikasyonuna 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 alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için kurduğumuz katalog ızgarasında bir kusur kaldı. Kartlar eşit yükseklikte ama içleri hizasız: Dune ve Emma'nın kapağı var, Middlemarch'ın yok. 1024px'te aynı satırdaki üç kartın başlığına baktığımızda Dune ve Emma'nınki kapağın altında, 201. pikselde; kapaksız Middlemarch'ınki ise kartın tepesinde, 17. pikselde. Fiyatlar margin-block-start: auto sayesinde hizalı ama başlık ve yazar satırları değil.
Şimdi kartı Flexbox'tan Grid'e çeviriyoruz ve ona kendi satırlarını tanımlatmak yerine şunu yazıyoruz: grid-row: span 4; grid-template-rows: subgrid;. Kartın içinde başka hiçbir şeyi değiştirmeden Middlemarch'ın başlığı kaçıncı piksele gelir? Kapaksız bir kartın başlığı neden kapaklı bir kartın kapağına göre konumlansın?
Bu yazıda Grid'in ikinci kısmına geçiyoruz. Önce farklı kartların satırlarını ortak bir ızgaraya bağlayan subgrid'i, sonra Grid ve Flexbox'ın hizalama property'lerini, en sonda da her layout işinde sorulan "Grid mi, Flexbox mı?" sorusunun karar kuralını ele alacağız. Masonry düzeninin Kasım 2026'daki durumuna da bakacağız.
Subgrid: Satırları Listeden Ödünç Almak
Bir grid item'ı kendisi de grid olabilir; buna nested grid denir. Sorun şu: içteki grid'in satırları dıştaki grid'den tamamen bağımsızdır. Her kart kendi satırlarını kendi içeriğine göre hesaplar ve yandaki kartın başlığının iki satır sürdüğünden haberi olmaz. subgrid değeri bunu değiştirir: kart kendi track'lerini tanımlamaz, ebeveyn grid'in kapladığı track'lerini kullanır. Lab sayfasında üç kartlık bir raf kurdum. Biri Flexbox'la, öteki subgrid'le:
/* her kart kendi flex kolonu: satırlar komşu kartlardan habersiz */
#without .card { display: flex; flex-direction: column; gap: 4px; }
#without .price { margin-block-start: auto; }
/* her kart raftan dört satır ödünç alır */
#with .card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
row-gap: 4px;
}
grid-row: span 4, kartın rafta dört satır kaplamasını söylüyor; grid-template-rows: subgrid ise bu dört satırı kartın kendi satırları yapıyor. Kartın dört child'ı (başlık, yazar, kısa açıklama, fiyat) bu satırlara sırayla oturuyor. Rafın satır yüksekliğini artık her satırdaki en uzun içerik belirliyor: Middlemarch'ın iki satırlık başlığı başlık satırını uzatıyor ve öteki iki kartın yazar satırı da onunla birlikte aşağı iniyor. Ölçüm script'i her kartın satırlarının rafın tepesine uzaklığını yazıyor:
node scripts/07-grid.mjs
--- chromium
#without
Dune author y=39 blurb y=67 price y=162
Middlemarch: A Stu author y=82 blurb y=110 price y=162
The Hobbit author y=39 blurb y=67 price y=162
#with
Dune author y=82 blurb y=110 price y=186
Middlemarch: A Stu author y=82 blurb y=110 price y=186
The Hobbit author y=82 blurb y=110 price y=186
#steps (display: grid)
Cart x=44 y=30 w=31
Address x=240 y=66 w=59
Payment x=508 y=30 w=63
#empty-cart (display: block)
Your cart is empty. x=0 y=88 w=1280
#chips-flex (display: flex)
Science Fiction x=0 y=0 w=136
Classics x=144 y=0 w=86
Children x=238 y=0 w=87
Poetry x=333 y=0 w=73
History of Science x=0 y=34 w=158
#chips-grid (display: grid)
Science Fiction x=0 y=0 w=135
Classics x=143 y=0 w=135
Children x=285 y=0 w=135
Poetry x=0 y=58 w=135
History of Science x=143 y=58 w=135
#covers (display: grid)
Dune x=0 y=0 w=100
Emma x=110 y=0 w=100
Ulysses x=220 y=0 w=100
Middlemarch x=0 y=160 w=100
The Hobbit x=110 y=160 w=100
İlk iki blok görselin özeti. Flexbox'ta yazar satırları 39, 82 ve 39. piksellerde, yani yalnız uzun başlıklı kartta aşağıda. Subgrid'de üç kartta da 82. pikselde. Fiyatlar her iki durumda da hizalı, ama farklı sebeplerle: Flexbox'ta margin-block-start: auto fiyatı kartın dibine itiyor, subgrid'de fiyat ortak bir satırda duruyor.

Subgrid yalnız bir eksende de kullanılabilir. Bu örnekte kolonları kart kendi tanımlıyor (aslında tek kolon var), satırları ise raftan alıyor. Tam tersi de mümkün: grid-template-columns: subgrid ile bir form satırındaki label ve input'ları sayfa ızgarasının kolonlarına bağlayabilirsin. Subgrid'li eksende implicit track oluşmaz; dört satır ödünç alan bir karta beşinci child koyarsan tarayıcı yeni satır açmaz, fazladan gelen item son satıra yerleşir. Kartın yapısını değiştirdiğinde span sayısını da güncellemen gerekiyor.
Bookstore Kataloğunda Subgrid
Aynı fikri katalog kartına uyguladım. Kapaklı ve kapaksız kartların karışık olduğu bir listede child'ları sırayla yerleştirmek yetmez: kapaksız kartın ilk child'ı başlık olduğu için kapak satırına oturur. Bu yüzden satırlara isim verdim ve her child'ı adıyla yerleştirdim:
/* her kart listeden dört satır ödünç alır; başlık, yazar ve fiyatlar satır boyunca hizalanır */
.book-card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
grid-template-areas: "cover" "title" "author" "price";
row-gap: 0.25rem;
position: relative;
padding: var(--space);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background-color: var(--color-surface);
}
.book-card > * {
margin: 0;
}
.book-card > img {
grid-area: cover;
justify-self: center;
}
.book-card .book-title {
grid-area: title;
}
.book-card .book-author {
grid-area: author;
}
/* fiyat satırı ortak; fiyat satırın alt kenarına oturur */
.book-card .price {
grid-area: price;
align-self: end;
}
Subgrid'li satırlara da grid-template-areas ile isim verilebilir; area'lar ödünç alınan satırların üzerine çizilir. Kapaksız Middlemarch'ın cover area'sı boş kalıyor, başlığı ise öteki kartların başlığıyla aynı satıra iniyor. Önceki yazıdaki rozet (.badge) position: absolute olduğu için grid item değil; satırlara hiç girmiyor ve kartın köşesinde kalmaya devam ediyor. Kartın kenar boşluğu ve çerçevesi de kaybolmuyor: subgrid'in padding'i ve border'ı ilk ve son satıra eklenir. Bir önceki yazının etiketinde (06) aynı ölçüm script'ini çalıştırınca durum buydu:
--- chromium 1024px
book-list rows: 302.797px 118.797px
Dune title y=201 author y=234 price y=262 rows: none
Middlemarch title y=17 author y=50 price y=262 rows: none
Emma title y=201 author y=234 price y=262 rows: none
The Hobbit title y=336 author y=369 price y=397 rows: none
Subgrid'den sonra:
node scripts/07-catalog.mjs
--- chromium 1024px
book-list rows: 191px 16.7969px 12px 35px 17px 16.7969px 12px 35px
Dune title y=201 author y=234 price y=262 rows: subgrid [] [] [] [] []
Middlemarch title y=201 author y=234 price y=262 rows: subgrid [] [] [] [] []
Emma title y=201 author y=234 price y=262 rows: subgrid [] [] [] [] []
The Hobbit title y=346 author y=379 price y=407 rows: subgrid [] [] [] [] []
Middlemarch'ın başlığı 17. pikselden 201. piksele indi; tahmin sorusunun cevabı bu. Üç kartın başlık, yazar ve fiyat satırları aynı hizada. WebKit aynı sayıları verdi. Kapaksız kartın tepesinde artık 180 piksellik boş bir alan var; katalogda istediğim de buydu, çünkü göz satır boyunca başlıkları tarıyor. Kapaksız kartlara bir yer tutucu görsel koymak bir tasarım kararı ve UI/UX Tasarım İlkeleri serisinin konusu.
Çıktının ilk satırı ilk bakışta tuhaf: liste dört satır yerine sekiz track gösteriyor ve başlık satırı yalnız 16.8px, oysa başlık 28.8px yüksekliğinde. Sebep row-gap. Liste satırlar arasında 16px boşluk bırakıyor, kart ise kendi içinde 4px istiyor. Subgrid'e farklı bir gap verdiğinde ebeveynin track'leri değişmez; aradaki fark, item'ların iki kenarına yarı yarıya dağıtılır. 16 ile 4 arasındaki 12px'lik fark, her item'ın üstünden ve altından 6px olarak düşülüyor, bu yüzden her track içeriğinden 12px kısa görünüyor. Ekranda sonuç doğru; ama DevTools'un grid çizgilerinde satır sınırları içerikle örtüşmüyor. Bunu bir hata sanıp düzeltmeye kalkma.
Hizalama: justify, align, -items, -content, -self ve place-*
Grid'in hizalama property'leri iki soruya cevap verir: hangi eksen ve neyi hizalıyorum? justify-* inline eksende (yatay yazımda soldan sağa), align-* block eksende (yukarıdan aşağıya) çalışır. -content track'lerin container içindeki yerini, -items bütün item'ların kendi hücresi içindeki yerini, -self tek bir item'ın yerini belirler. Lab sayfasında 600px'lik bir container'a üç tane 120px'lik kolon koydum, yani 240px boş alan var:
/* track'ler container'dan küçük: dağıtılacak boş alan var */
.steps {
display: grid;
grid-template-columns: repeat(3, 120px);
grid-template-rows: 60px;
inline-size: 600px;
block-size: 120px;
justify-content: space-between; /* track'leri container içinde taşır */
align-content: center;
justify-items: center; /* her item'ı kendi hücresi içinde taşır */
}
.steps .current { place-self: end start; }
/* sıradan bir block container'da align-content: flex yok, grid yok */
.empty-cart { display: block; block-size: 200px; align-content: center; }
| Property | Neyi hareket ettirir | Grid | Flexbox |
|---|---|---|---|
justify-content / align-content | Track'leri ya da satırları, container'daki boş alan içinde | Track toplamı container'dan küçükse | Main axis'te item'ları; align-content yalnız çok satırlı (wrap) container'da |
justify-items / align-items | Her item'ı kendi hücresi içinde | İki eksende de çalışır | justify-items yok sayılır; align-items cross axis'te çalışır |
justify-self / align-self | Tek bir item'ı | İki eksende de çalışır | justify-self yok sayılır; main axis'te margin: auto kullanılır |
place-content, place-items, place-self | Önce align, sonra justify değeri | Kısaltma | Kısaltma; yok sayılan yarısı etkisiz |
Çıktıda #steps bloğu: justify-content: space-between üç track'i 0, 240 ve 480. piksellere dağıttı, justify-items: center her adımı kendi 120px'lik hücresinin ortasına koydu. "Cart" 31px genişliğinde ve 44. pikselde başlıyor; (120 − 31) / 2 = 44.5. align-content: center 60px'lik satırı 120px'lik container'ın ortasına, 30. piksele indirdi. "Address" ise place-self: end start ile hücresinin sol alt köşesinde: x=240, y=66 (satırın bittiği 90'dan kendi 24px'lik yüksekliği çıkınca). place-* kısaltmalarında ilk değer block ekseni (align), ikinci değer inline ekseni (justify); sırayı karıştırmak en sık yapılan hata.
Bu tabloda olmayan ama 2024'ten beri tarayıcılarda olan bir yenilik daha var: align-content artık sıradan block container'larda da çalışıyor. Çıktıdaki #empty-cart, 200px yüksekliğinde display: block bir kutu. "Your cart is empty." metni 88. pikselde, yani tam ortada; (200 − 24) / 2 = 88. Boş sepet mesajını dikey ortalamak için artık element'i flex ya da grid container yapman gerekmiyor. Bu özellik Nisan 2024'te Baseline oldu ve Ekim 2026'dan beri widely available.
Grid mi, Flexbox mı?
İkisi de bir dizi element'i yan yana ve alt alta koyabiliyor; fark, boyutun nereden geldiğinde. Lab sayfasında aynı beş kategori etiketini 420px'lik iki listeye koydum. Biri Flexbox, öteki Grid:
.chips { inline-size: 420px; gap: 8px; }
.chips li { padding-inline: 12px; border: 1px solid #e5e7eb; }
.chips.flex { display: flex; flex-wrap: wrap; }
.chips.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
Çıktıdaki #chips-flex'te her etiket içeriği kadar genişlikte: "Classics" 86px, "History of Science" 158px; ilk satıra dördü sığdı, beşincisi alta indi. #chips-grid'de ise hepsi 135px; "Science Fiction" bu genişliğe sığmadığı için iki satıra kırıldı ve ilk satırın yüksekliği iki katına çıktı (ikinci satır 58. pikselde başlıyor). Kategori etiketleri için doğru araç Flexbox, çünkü her etiketin genişliğini içeriği belirlemeli.
Benim kullandığım kural şu: boyut içerikten dışarı doğru geliyorsa Flexbox, layout'tan içeri doğru geliyorsa Grid. Flexbox'ta önce item'lar vardır; container onları bir eksende dizer ve kalan alanı paylaştırır. Grid'de önce ızgara vardır; item'lar hazır hücrelere yerleşir ve satırlar ile kolonlar birbirini bilir. Navbar, buton grubu, etiket listesi, input ile butonu yan yana koymak birinci türden. Sayfa iskeleti, kart ızgarası, label'ları hizalı form ve satırları hizalı kartlar ikinci türden.
Gerçek sayfalar ikisini birlikte kullanır ve Bookstore'un katalog sayfası bunun iyi bir örneği. Sayfa iskeleti Grid (grid-template-areas), içindeki header Flexbox (logo, arama, sepet), sonuç listesi Grid, her kart subgrid, checkout'taki kupon alanı yine Flexbox. Seçim sayfa bazında değil, container bazında yapılıyor. "Her şeyi Grid'le yaparım" demek de "Grid'i hiç kullanmam" demek de aynı derecede gereksiz kod üretir.
Masonry ve grid-lanes: Kasım 2026 Durumu
Grid'in hâlâ tek başına yapamadığı bir düzen var: farklı yükseklikteki kapakları sütunlara, boşluk bırakmadan, her yeni öğeyi en kısa sütunun altına koyarak dizmek. Bu masonry düzeni yıllarca JavaScript kütüphaneleriyle yapıldı. Spesifikasyon tarafında uzun bir tartışmadan sonra ayrı bir display değeri, display: grid-lanes, kabul edildi. Kasım 2026'da durum şu: web-features 3.46 verisine göre yalnız Safari 27.2 destekliyor, Chrome ve Firefox'ta yok; yani Baseline değil. Ölçtüğüm Chrome 158'de CSS.supports('display', 'grid-lanes') false döndü, WebKit 27.2'de true. Bu yüzden lab sayfasında onu yalnız @supports içinde, normal bir ızgaranın üstüne ekledim:
/* masonry: yalnız grid-lanes olan tarayıcıda, öteki her yerde normal grid */
.covers { display: grid; grid-template-columns: repeat(3, 100px); gap: 10px; inline-size: 320px; }
@supports (display: grid-lanes) {
.covers { display: grid-lanes; }
}
Chromium'da kapaklar normal ızgarada: Middlemarch ve The Hobbit ikinci satırda, ilk satırın en uzun kapağı olan Dune'un bittiği 160. pikselde. WebKit'in çıktısı farklı:
--- webkit
#covers (display: grid-lanes)
Dune x=0 y=0 w=100
Emma x=110 y=0 w=100
Ulysses x=220 y=0 w=100
Middlemarch x=110 y=100 w=100
The Hobbit x=220 y=130 w=100
Middlemarch, 90px'lik Emma'nın altındaki sütuna (y=100), The Hobbit 120px'lik Ulysses'in altına (y=130) yerleşti; Dune'un sütununda yer kalmasına rağmen oraya gitmediler, çünkü o sütun en uzunu. Görsel olarak sıkı bir düzen ama okuma sırası artık zikzak çiziyor: ekranda göz soldan sağa, yukarıdan aşağıya ilerlerken DOM (tarayıcının sayfadan kurduğu element ağacı) sırası sütunlar arasında atlıyor. Bir katalogda sıralama önemliyse (fiyata göre sıralanmış sonuçlar gibi) masonry yanlış araç. Kapak galerisi ya da kullanıcı yorumları gibi sırasının anlamı olmayan içerikte @supports ile ilerleyici iyileştirme (progressive enhancement) olarak kullanılabilir; desteklemeyen tarayıcıda düzgün bir ızgara görünür.
Üretimde Dikkat
Subgrid Eylül 2023'te, Chrome 117 ile Baseline oldu (Firefox 2019'dan, Safari 2022'den beri destekliyordu) ve Mart 2026'dan beri widely available. Bugün kullanmak için fallback yazman gerekmiyor. Daha eski tarayıcıları desteklemen gereken bir projedeysen, desteklemeyen tarayıcı grid-template-rows: subgrid'i geçersiz sayar ve kart tek kolonlu, kendi satırlarını hesaplayan sıradan bir grid olarak kalır; içerik okunur, yalnız hizalama kaybolur.
Subgrid'in bedeli, kartın yapısının listeye bağlanması. span 4 kartın dört satırlı olduğunu varsayıyor; karta yeni bir satır eklemek (örneğin stok durumu) hem span'ı hem area listesini değiştirmeyi gerektiriyor. Bir de şuna dikkat et: subgrid, ebeveynin layout'una katılmayı gerektirir. Kartı kendi başına bir layout bağlamına alan her şey (ileride göreceğimiz container-type dahil) subgrid'i sessizce kapatır. Bunu serinin container query yazısında ölçeceğiz.
Hizalama testleri için ekran görüntüsü karşılaştırması yerine bu yazıdaki gibi konum okuyan bir script daha kararlı sonuç verir: "üç kartın yazar satırı aynı y'de mi?" sorusu, piksel farkı aramaktan daha net bir test.
Sık Düşülen Tuzaklar
Birincisi, grid-row: span N'i unutmak. Subgrid, kartın kapladığı track'leri ödünç alır; kart tek satır kaplıyorsa tek satır ödünç alır ve bütün child'lar o satıra yığılır.
İkincisi, child'ları sırayla yerleştirmek. Bazı kartlarda olmayan bir element (kapak, rozet, indirim etiketi) varsa sıralı yerleştirme bütün satırları kaydırır; area isimleriyle yerleştir.
Üçüncüsü, place-* kısaltmalarında değer sırasını karıştırmak: önce align (block ekseni), sonra justify (inline ekseni). Flexbox'ta justify-items ve justify-self'in hiçbir etkisi olmadığını da unutma; main axis'te tek bir item'ı itmek için margin-inline-start: auto kullanılır.
Dördüncüsü, masonry için tek tarayıcıdaki desteğe güvenmek. grid-lanes Baseline değil; Önüne bir display: grid yazmadan ve @supports olmadan kullanılan display: grid-lanes, Chrome'da geçersiz bir declaration olarak atılır ve element varsayılan display değeriyle, block olarak kalır.
Küçük Bir Deneme
★☆☆ lab/07-subgrid.html'deki #with .card'dan row-gap: 4px'i sil. Kartların içindeki boşluğun kaç piksel olacağını tahmin et, sonra node scripts/07-grid.mjs ile kontrol et.
★★☆ catalog.html'de Dune kartına, başlıkla yazar arasına bir p.book-series ("Dune Chronicles, Book 1") ekle. span değerini ve grid-template-areas listesini buna göre güncelle; öteki kartlarda bu satırın boş kaldığını ve başlıkların hâlâ hizalı olduğunu script'le doğrula.
★★★ checkout.html'deki form satırlarını tek bir Grid'e çevir: label'lar solda aynı genişlikte bir kolonda, input'lar sağda. Her .form-row'u grid-template-columns: subgrid ile ebeveyn kolonlarına bağla ve en uzun label'ın kolonu belirlediğini ölçen bir Playwright script'i yaz.
Bir sonraki yazıda ızgaradan sayfaya çıkıyoruz: mobile-first yazım, media query range syntax'ı, clamp() ile zoom'u bozmayan fluid typography ve kullanıcının işletim sisteminde seçtiği tercihleri (koyu tema, azaltılmış hareket, yüksek kontrast, dokunmatik ekran) okuyan media feature'lar.