Flexbox: Tek Boyutlu Layout, flex-grow/shrink/basis ve Gerçek Hayat Senaryoları
Flexbox boş alanı flex-grow oranında dağıtır, eksik alanı ise flex-shrink × flex-basis ağırlığıyla keser; yani büyük item daha çok küçülür. Bookstore'un header'ını, katalog kartlarını ve kupon alanını Flexbox ile diziyor, flex kısaltmalarının açılımını, uzun kitap başlığını satırdan dışarı iten min-width: auto tuzağını ve order'ın Tab sırasını değiştirmediğini Chromium ve WebKit'te ölçüyoruz.
Sürüm notu: Bu yazı CSS Flexible Box Layout Level 1 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 verisinden alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için sepet satırını tasarlıyoruz: solda kapak, ortada kitap bilgisi, sağda butonlar. Üç parçaya farklı flex değerleri verdim:
@layer components {
.row { display: flex; inline-size: 600px; }
.row.narrow { inline-size: 300px; }
.row > * { margin: 0; outline: 1px solid #94a3b8; }
.cover { flex: 1 1 100px; }
.details { flex: 2 1 100px; }
.actions { flex: 1 1 200px; }
}
Satır 600px genişliğindeyken üç parça kaçar piksel olur? Ve aynı satır 300px'e daraldığında? Çoğu kişinin ilk tahmini daralma için "üçü de aynı oranda küçülür" ya da "flex-shrink'leri eşit, o halde her biri eşit miktarda küçülür" oluyor. İkisi de yanlış.
Önceki yazıda normal flow'u ve konumlandırmayı konuştuk. Bu yazıda layout'un ilk büyük aracına geçiyoruz: tek boyutlu layout için tasarlanmış Flexbox. Boş alanın nasıl paylaştırıldığını sayılarla hesaplayacak, sonra Bookstore'un header'ını, katalog kartlarını ve kupon alanını Flexbox ile dizeceğiz.
Main Axis, Cross Axis ve gap
Bir element'e display: flex verdiğinde doğrudan child'ları flex item olur ve bir eksen boyunca dizilir. Bu eksen main axis: flex-direction: row'da (varsayılan) satırın yazıldığı yön, column'da alt alta. Ona dik olan eksen cross axis. Flexbox'ın bütün hizalama property'leri bu iki eksene göre çalışır: justify-content item'ları main axis boyunca, align-items cross axis boyunca hizalar. flex-direction: column yazdığında ikisinin anlamı yer değiştirir; "yatayda ortalamak için hangisiydi?" karışıklığının sebebi bu.

Item'lar arasındaki boşluğu margin yerine gap ile ver. Önceki yazıda gördüğümüz gibi flex item'larında margin'ler collapse etmez ve iki tarafa margin vermek boşluğu iki katına çıkarır; gap ise yalnız item'ların arasına, kenarlara değil, tek bir değer koyar. Flexbox'ta gap Nisan 2021'den beri Baseline. Item'lar sığmadığında alt satıra geçmeleri için flex-wrap: wrap gerekir; varsayılan nowrap'ta item'lar sığmasalar da tek satırda kalıp küçülmeye çalışır.
flex-grow, flex-shrink ve flex-basis: Sayılarla
Her flex item'ın boyutu üç adımda hesaplanır. Önce flex-basis: item'ın paylaştırmadan önceki başlangıç boyutu. Sonra container'daki boş alan bulunur: container genişliği eksi basis'lerin toplamı. Boş alan pozitifse flex-grow oranında dağıtılır, negatifse item'lar flex-shrink ile küçülür. Lab sayfasını iki motorda ölçtüm:
node scripts/05-flex.mjs
node scripts/05-pages.mjs
--- chromium
wide (600px): cover 150, details 200, actions 250
narrow (300px): cover 75, details 75, actions 150
flex: initial -> grow 0, shrink 1, basis auto
flex: 1 -> grow 1, shrink 1, basis 0%
flex: auto -> grow 1, shrink 1, basis auto
flex: none -> grow 0, shrink 0, basis auto
flex: 12rem -> grow 1, shrink 1, basis 192px
broken details 556.8px, price right edge 656.4px, line right edge 320px
fixed details 220.4px, price right edge 320.0px, line right edge 320px
visual order: Add to cart | Details | Add to wishlist
Tab order: Details | Add to wishlist | Add to cart
--- webkit
wide (600px): cover 150, details 200, actions 250
narrow (300px): cover 75, details 75, actions 150
flex: initial -> grow 0, shrink 1, basis auto
flex: 1 -> grow 1, shrink 1, basis 0%
flex: auto -> grow 1, shrink 1, basis auto
flex: none -> grow 0, shrink 0, basis auto
flex: 12rem -> grow 1, shrink 1, basis 192px
broken details 556.8px, price right edge 656.4px, line right edge 320px
fixed details 220.4px, price right edge 320.0px, line right edge 320px
visual order: Add to cart | Details | Add to wishlist
600px'lik satırda basis'lerin toplamı 100 + 100 + 200 = 400, boş alan 200px. Grow değerleri 1 : 2 : 1, yani 200px dörde bölünüp 50, 100 ve 50 olarak dağıtıldı: 150, 200, 250. Buraya kadar sezgisel.
300px'lik satırda eksik alan 100px. Shrink değerleri eşit (1, 1, 1) ama küçülme eşit dağıtılmadı: 75, 75, 150. Spec'e göre küçülme flex-shrink ile değil, flex-shrink × flex-basis ile ağırlıklanır. Ağırlıklar 100 : 100 : 200, yani eksik 100px'in dörtte biri ilk ikisinden, yarısı üçüncüden kesildi. Mantığı şu: büyük bir item'dan 25px kesmek onun boyutunun küçük bir kısmıdır, ama küçük bir item'dan aynı miktarı kesmek onu yok edebilir. Bu yüzden büyük olan daha fazla küçülür.
Gerçek hayatta bu hesabı nadiren yapacaksın, çünkü flex kısaltmasının dört yaygın değeri çoğu ihtiyacı karşılıyor. Çıktının ikinci kısmı onların açılımını gösteriyor:
flex: initial(varsayılan) →0 1 auto: büyümez, gerekirse küçülür, içeriği kadar başlar.flex: 1→1 1 0%: basis sıfır, bütün alan grow oranında paylaşılır; eşit genişlikte kolonlar için.flex: auto→1 1 auto: içeriği kadar başlar, kalan alan paylaşılır; içeriği büyük olan daha geniş olur.flex: none→0 0 auto: hiç büyümez, hiç küçülmez; fiyat etiketi gibi bölünmemesi gereken parçalar için.
Tek bir uzunluk yazmak (flex: 12rem) ise 1 1 12rem demek. flex: 1 ile flex: auto arasındaki fark çoğu zaman gözden kaçar: flex: 1'li üç sekme, metinleri farklı uzunlukta olsa da eşit genişlikte olur; flex: auto'lu sekmelerde uzun metinli sekme daha geniş kalır.
Gerçek Hayat Senaryoları: Header, Kart ve Input Grubu
İlk senaryo header. Logo, arama kutusu ve sepet linki tek satırda, sepet en sağda:
<header class="site-header">
<a class="logo" href="catalog.html">Bookstore</a>
<form class="site-search" role="search" action="catalog.html">
<input type="search" name="q" placeholder="Search books" aria-label="Search books">
</form>
<a class="cart-link" href="cart.html">Cart <span class="cart-count"></span></a>
</header>
.site-header {
display: flex;
align-items: center;
gap: var(--space);
position: sticky;
inset-block-start: 0;
z-index: 10;
block-size: 3.5rem;
padding-inline: var(--space);
background-color: var(--color-surface);
border-block-end: 1px solid var(--color-border);
}
.site-search {
flex: 1 1 12rem;
max-inline-size: 30rem;
}
.site-search input {
inline-size: 100%;
}
/* auto margin bütün boş alanı alır ve sepeti sona iter */
.cart-link {
margin-inline-start: auto;
}
.cart-count:empty {
display: none;
}
/* klavye kullanıcısı belirgin bir çerçeve görür, fare kullanıcısı görmez */
:where(a, button, input, summary):focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 2px;
}
align-items: center üçünü dikeyde ortalıyor. Sepeti sağa itmek için ayrı bir sarmalayıcı ya da justify-content: space-between kullanmadım: margin-inline-start: auto, main axis'teki bütün boş alanı o margin'e verir ve sepeti sona iter. Flexbox'ta auto margin'in bu davranışı, "bir item'ı ayır, gerisini bir arada tut" ihtiyacının en temiz çözümü.
İkinci senaryo katalog kartları. Kartların her biri bir flex container (flex-direction: column), liste ise satır satır dizen ve sarmalayan bir flex container:
.book-list {
display: flex;
flex-wrap: wrap;
gap: var(--space);
list-style: none;
margin: 0;
padding: 0;
/* listenin içindeki z-index değerleri sticky header ile hiçbir zaman yarışmaz */
isolation: isolate;
}
.book-card {
flex: 1 1 14rem;
display: flex;
flex-direction: column;
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 {
align-self: center;
}
/* başlık ne kadar uzun olursa olsun fiyat en altta durur */
.book-card .price {
margin-block-start: auto;
}
/* kapak görseli olmayan kart onun yerine renkli bir zemin alır */
.book-card:not(:has(> img)) {
background-color: #fef3c7;
}
/* zebra çizgileri yalnız stokta olan kitapları sayar */
.book-card:nth-child(even of .in-stock) {
border-color: #93c5fd;
}
.book-card[data-stock="0"] .price {
text-decoration: line-through;
color: var(--color-muted);
}
.book-card[data-format~="ebook"] .book-title::after {
content: " · e-book";
font-size: 0.75em;
color: var(--color-muted);
}
.badge {
position: absolute;
inset-block-start: 0.5rem;
inset-inline-end: 0.5rem;
z-index: 1;
padding-inline: 0.5rem;
border-radius: var(--radius);
background-color: var(--color-accent);
color: #ffffff;
font-size: 0.75rem;
}
--- chromium
1024px header: logo x=16, search 105.4-585.4, cart right edge 1008 of 1024
Dune x=16.0 y=177 w=236.0 h=302.8 price bottom gap 17px
Middlemarch x=268.0 y=177 w=236.0 h=302.8 price bottom gap 17px
Emma x=520.0 y=177 w=236.0 h=302.8 price bottom gap 17px
The Hobbit x=772.0 y=177 w=236.0 h=302.8 price bottom gap 17px
768px header: logo x=16, search 105.4-585.4, cart right edge 752 of 768
Dune x=16.0 y=177 w=234.7 h=302.8 price bottom gap 17px
Middlemarch x=266.7 y=177 w=234.7 h=302.8 price bottom gap 17px
Emma x=517.3 y=177 w=234.7 h=302.8 price bottom gap 17px
The Hobbit x=16.0 y=496 w=736.0 h=118.8 price bottom gap 17px
coupon group 320px: input 234.1px, button 77.9px, button right edge 320
--- webkit
1024px header: logo x=16, search 105.4-585.4, cart right edge 1008 of 1024
Dune x=16.0 y=178 w=236.0 h=302.8 price bottom gap 17px
Middlemarch x=268.0 y=178 w=236.0 h=302.8 price bottom gap 17px
Emma x=520.0 y=178 w=236.0 h=302.8 price bottom gap 17px
The Hobbit x=772.0 y=178 w=236.0 h=302.8 price bottom gap 17px
768px header: logo x=16, search 105.4-585.4, cart right edge 752 of 768
Dune x=16.0 y=178 w=234.7 h=302.8 price bottom gap 17px
Middlemarch x=266.7 y=178 w=234.7 h=302.8 price bottom gap 17px
Emma x=517.3 y=178 w=234.7 h=302.8 price bottom gap 17px
The Hobbit x=16.0 y=497 w=736.0 h=118.8 price bottom gap 17px
coupon group 320px: input 234.1px, button 77.9px, button right edge 320
1024px'te dört kart yan yana, her biri 236px ve hepsi aynı yükseklikte (302.8px). Eşit yükseklik için hiçbir şey yazmadım: align-items'ın varsayılanı stretch ve aynı satırdaki item'lar satırın yüksekliğine uzar. Kapaksız kartlar kısa içerikli olsa da kapaklılar kadar uzun. Fiyatın her kartta alttan 17px'te (16px padding + 1px border) durması ise .price'a verilen margin-block-start: auto'dan geliyor; header'daki auto margin tekniğinin aynısı, bu kez column yönündeki main axis'te. Kartlardaki img'e align-self: center verdim; yoksa stretch kapağı kart genişliğine uzatırdı.
768px'teki sonuç Flexbox'ın sınırını gösteriyor. İlk üç kart 234.7px, ama dördüncü kart (The Hobbit) alt satırda tek başına kaldı ve 736px'e uzadı. flex-wrap'ta her satır kendi içinde bağımsız hesaplanır; alt satırdaki tek item bütün boş alanı alır. Kartların her satırda aynı kolonlara oturmasını istiyorsan bu iki boyutlu bir problem ve doğru araç Grid. Katalog listesini bir sonraki yazıda Grid'e taşıyacağız.
Üçüncü senaryo checkout'taki kupon alanı: input esnesin, buton içeriği kadar kalsın.
.input-group {
display: flex;
gap: 0.5rem;
}
/* min-inline-size: 0, input'un varsayılan genişliğinin altına küçülebilmesini sağlar */
.input-group input {
flex: 1;
min-inline-size: 0;
}
Çıktının son satırında 320px'lik telefonda input 234.1px, buton 77.9px ve butonun sağ kenarı tam 320'de: taşma yok. Buradaki min-inline-size: 0 bir sonraki bölümün konusu.
min-width: 0 Tuzağı ve Uzun Kitap Başlıkları
Flex item'ların min-width'inin varsayılanı 0 değil auto, ve flex item için auto "içeriğin min-content genişliğinden daha dar olma" demek. Çoğu zaman bu iyi bir şey: bir kelimenin ortadan kesilmesini engeller. Ama içeriğin min-content genişliği çok büyükse sorun olur. Sepet satırında kitap başlığını tek satırda üç noktayla kesmek istedim:
@layer components {
.cart-line { display: flex; gap: 1rem; inline-size: 320px; outline: 1px solid #94a3b8; }
.cart-line p { margin: 0; }
.cart-line .details { flex: 1; }
.cart-line .title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cart-line .price { flex: none; }
.cart-line.fixed .details { min-inline-size: 0; }
}
<div class="cart-line" id="broken">
<div class="details">
<p class="title">Twenty Thousand Leagues Under the Seas: An Underwater Tour of the World</p>
<p class="author">Jules Verne</p>
</div>
<span class="price">189.00 TRY</span>
</div>
Çıktıdaki broken satırına bak: 320px'lik satırda .details 556.8px'e çıktı ve fiyatın sağ kenarı 656.4px'te, yani satırın 336px dışında. .title'da white-space: nowrap olduğu için .details'in min-content genişliği başlığın tamamı kadar oldu ve min-width: auto onun bu genişliğin altına küçülmesine izin vermedi. Başlık üç noktayla kesilmedi, fiyatı satırın dışına itti. fixed satırında tek fark .details'e min-inline-size: 0 yazmak: .details 220.4px'e küçüldü, başlık kesildi, fiyat satırın içinde. Önceki yazının sonunda söz verdiğim tuzak buydu.
Bir ayrıntı: overflow'u visible olmayan bir flex item'ın otomatik minimum boyutu zaten sıfırdır. Bu yüzden overflow: hidden'ı doğrudan flex item'a yazsaydım sorun çıkmazdı. Sorun, kesme işini item'ın içindeki bir element yaptığında ortaya çıkıyor. Kupon alanında da aynı sebeple input'a min-inline-size: 0 verdim: input'ların tarayıcının belirlediği bir varsayılan genişliği var ve dar bir ekranda bu, butonu dışarı itmeye yetiyor.
order: Görsel Sıra ve DOM Sırası
order property'si item'ların görsel sırasını DOM'u (tarayıcının sayfadan kurduğu element ağacı) değiştirmeden değiştirir. Kart aksiyonlarında "Add to cart" butonunu öne almak için order: -1 verdim:
@layer components {
.card-actions { display: flex; gap: 0.5rem; }
.card-actions .add-to-cart { order: -1; }
}
<div class="card-actions">
<a href="#details">Details</a>
<a href="#wishlist">Add to wishlist</a>
<button class="btn btn-primary add-to-cart" type="button">Add to cart</button>
</div>
Çıktının son iki satırı: görsel sıra "Add to cart | Details | Add to wishlist", ama Tab ile gezildiğinde sıra "Details | Add to wishlist | Add to cart". Klavye kullanıcısı ve ekran okuyucu DOM sırasını izler, ekrandaki sırayı değil. Görsel sıra ile odak sırası birbirinden koptuğunda klavye kullanıcısının odağı ekranda zıplıyor gibi görünür. Bu ölçümü yalnız Chromium'da yaptım; önceki yazılarda değindiğim gibi WebKit'in macOS üzerinde Tab ile butonlara gitmemesi karşılaştırmayı anlamsız kılıyor.
Kural basit: sıra anlamlıysa DOM'u değiştir, order'ı değil. order'ı ve flex-direction: row-reverse'u yalnız sıranın anlam taşımadığı, tamamen görsel durumlarda kullan. Chrome'da odak sırasını görsel sıraya göre ayarlayan reading-flow property'si var, ama Kasım 2026 itibarıyla yalnız Chromium tabanlı tarayıcılarda ve Baseline değil. Bunun erişilebilirlik gerekçesi ilerideki Accessibility (a11y) serisinde.
Üretimde Dikkat
Flexbox'ı içerikten dışa doğru düşünülen layout'lar için kullan: bir satırdaki butonlar, bir header, bir form satırı, bir kartın içindeki dikey düzen. Item'ların boyutunu içerikleri belirliyor ve container onları sığdırmaya çalışıyorsa Flexbox doğru araç. Kartların her satırda aynı kolonlara oturması gibi layout'tan içeriğe doğru bir düzen istiyorsan Grid daha uygun; bu kararı serinin subgrid ve "Grid mi Flexbox mı?" yazısında ayrıntılı ele alacağız.
Gerçek içerikle test et. Bu yazıdaki iki hata, 768px'te tek başına uzayan kart ve fiyatı satırdan iten başlık, kısa test verisiyle hiç görünmez. Bookstore'un en uzun kitap başlığı, en uzun yazar adı ve hiç kapağı olmayan bir kitap, layout testlerinin vazgeçilmez verisi olmalı. Bu yazıdaki Playwright script'leri gibi birkaç genişlikte boyut ölçen kontroller, bu hataları görsel regresyon aracı kurmadan yakalıyor.
Sık Düşülen Tuzaklar
Birincisi, flex-shrink'in eşit küçülme sağladığını sanmak. Küçülme flex-shrink × flex-basis ile ağırlıklanır; büyük item daha çok küçülür. Bir item'ın hiç küçülmemesini istiyorsan flex: none ya da flex-shrink: 0 ver.
İkincisi, uzun içerikli flex item'ın taşması. Sebep neredeyse her zaman min-width: auto; çözüm item'a min-inline-size: 0 ya da overflow: hidden vermek.
Üçüncüsü, flex-wrap ile kart ızgarası kurmak. Son satırdaki item'lar diğerlerinden farklı genişlikte olur. Kolonların hizalı olması gerekiyorsa Grid kullan.
Dördüncüsü, order ile anlamlı sırayı değiştirmek. Ekrandaki sıra ile Tab sırası ayrışır.
Küçük Bir Deneme
★☆☆ lab/05-flex-math.html'de .actions'ın flex-shrink'ini 0 yap ve 300px'lik satırın yeni genişliklerini önce kâğıt üzerinde hesapla, sonra node scripts/05-flex.mjs ile kontrol et.
★★☆ Katalog kartlarındaki flex: 1 1 14rem'i flex: 0 1 14rem yap. 768px'te tek kalan kartın genişliği ne oldu? Bu kez ne kaybettin?
★★★ Sepet sayfası için Flexbox ile bir satır yaz: kapak, başlık ve yazar, adet seçici, fiyat ve silme butonu. En uzun başlık ve 320px genişlikte hiçbir şeyin taşmadığını ve fiyatın her satırda aynı hizada kaldığını bir Playwright script'iyle doğrula. Fiyatların hizası için Flexbox'ın yetmediği noktayı not et; cevabı bir sonraki yazıda.
Bir sonraki yazıda Grid'e geçiyoruz: track'ler, fr birimi, media query'siz responsive katalog ızgarası kuran repeat(auto-fill, minmax()) ve sayfa iskeletini isimle tarif eden grid-template-areas.