Mert Özen Her Satırda Daha İleri
Frontend Tailwind CSS — #07

Günlük Utility Aileleri 1: Layout, Flex, Grid, Spacing, Sizing ve Position Utility'leri

Mert Özen 15 Kas 2026 12 dk 5 görüntülenme
Günlük Utility Aileleri 1: Layout, Flex, Grid, Spacing, Sizing ve Position Utility'leri

Tailwind'in layout utility'leri CSS'e bire bir karşılık gelir ve sayısal değerler tek bir --spacing değişkeninden türer; zor kısım hangi CSS'i seçeceğini bilmek. Bookstore'un katalog grid'ini, kitap kartını ve iki kolonlu sepet sayfasını hem utility'lerle hem düz CSS ile kuruyor, iki sayfanın kutularını üç genişlikte Playwright ile karşılaştırıyor (fark 0px), space-x'in wrap olan satırlardaki hatasını, eksik min-w-0'ı ve auto-fill ile auto-fit farkını ölçüyoruz.

Sürüm notu: Bu yazıdaki örnekler Tailwind CSS 4.3.5, Node.js 24 ve Playwright 1.65 (Chromium 158) ile denenmiştir.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), katalog sayfasının üstünde tür etiketlerini gösteriyor. Etiketler 320px genişliğinde bir kutuda, sığmayanlar alt satıra geçiyor ve satırlar ortalanıyor. Etiketlerin arasına boşluk koymak için ilk akla gelen class space-x-4:

<div id="chips-space" class="flex w-80 flex-wrap justify-center space-x-4">
    <span class="rounded-full bg-white px-3 py-1">Fantasy</span>
    <span class="rounded-full bg-white px-3 py-1">Science fiction</span>
    <span class="rounded-full bg-white px-3 py-1">Classics</span>
    <span class="rounded-full bg-white px-3 py-1">Poetry</span>
</div>

Dört etiket iki satıra bölünüyor. İki satırın arasında boşluk olur mu? İlk satır gerçekten ortada durur mu?

Önceki yazılarda Tailwind'in configuration'ını, token'larını, variant'larını ve entegrasyonunu kurduk. Bu yazı ve bir sonraki, her gün yazdığımız utility ailelerine ayrılıyor. Bu yazıda layout tarafı var: display, flex, grid, spacing, sizing, position ve responsive prefix'ler. Örnekler Bookstore'un katalog grid'i, kitap kartı ve sepet sayfası; her ekranın düz CSS karşılığını da yazıp Playwright ile iki sayfanın kutularını piksel piksel karşılaştırıyoruz. Altta yatan CSS mekanizmaları CSS3 serisinin Box Model, Flexbox, CSS Grid ve Responsive Design yazılarında.

Utility'yi Ürettiği CSS'le Okumak

Bir utility ailesini öğrenmenin en kısa yolu, class'ın hangi CSS satırına dönüştüğüne bakmak. Variant yazısında kullandığımız 04-selectors.mjs her class'ı ayrı derleyip yalnız utility kuralını basıyor:

node scripts/04-selectors.mjs 'p-4' 'p-13' 'mx-auto' 'size-10' 'w-1/2' 'max-w-7xl'
p-4
  .p-4 {
    padding: calc(var(--spacing) * 4);
  }

p-13
  .p-13 {
    padding: calc(var(--spacing) * 13);
  }

mx-auto
  .mx-auto {
    margin-inline: auto;
  }

size-10
  .size-10 {
    width: calc(var(--spacing) * 10);
    height: calc(var(--spacing) * 10);
  }

w-1/2
  .w-1\/2 {
    width: calc(1 / 2 * 100%);
  }

max-w-7xl
  .max-w-7xl {
    max-width: var(--container-7xl);
  }

Üç şey hemen görünüyor. Birincisi, spacing ölçeği tek bir değişkenden türüyor: --spacing varsayılan temada 0.25rem ve her sayısal utility onun katı. v3'te p-13 yoktu, çünkü ölçek bir tablodan geliyordu; v4'te herhangi bir tam sayı ya da 2.5 gibi buçuklu değer çalışıyor. Theme yazısında anlattığımız gibi @theme { --spacing: ... } ile bütün ölçeği tek satırda değiştirebilirsin. İkincisi, mx-auto ve px-4 artık margin-inline ve padding-inline üretiyor; yatay eksen metin yönüne bağlı logical property'lerle yazılmış. Üçüncüsü, her boyut aynı ölçekten gelmiyor: max-w-7xl spacing'den değil --container-* namespace'inden okuyor, w-1/2 ise yüzde hesaplıyor.

Display ve Flex: Kitap Kartının İskeleti

Katalogdaki her kitap bir kart: üstte kapak, altında başlık ve yazar, en altta fiyat ve "Add" butonu. Başlıklar farklı uzunlukta olsa da aynı satırdaki kartların footer'ı aynı hizada durmalı:

<article class="flex h-full flex-col overflow-hidden rounded-lg border border-slate-200 bg-white">
    <div class="relative aspect-2/3 bg-amber-200">
        <span id="badge" class="absolute inset-s-2 top-2 z-10 rounded bg-white px-2 py-0.5 text-xs">New</span>
    </div>
    <div class="flex flex-1 flex-col gap-1 p-4">
        <h2 class="line-clamp-2 font-semibold">Dune</h2>
        <p class="text-sm text-slate-600">Frank Herbert</p>
        <div class="mt-auto flex items-center justify-between pt-3">
            <span class="font-semibold">420.00 TRY</span>
            <button type="button" class="rounded bg-indigo-600 px-3 py-1 text-sm text-white">Add</button>
        </div>
    </div>
</article>

İskeleti üç class taşıyor. Kart flex flex-col h-full ile grid hücresinin bütün yüksekliğini alıyor; gövde flex-1 ile kapaktan kalan alanı dolduruyor; footer mt-auto ile gövdenin en altına itiliyor. mt-auto bir flex container'ın içinde kalan boş alanı o kenara topluyor, bu yüzden başlık bir satır da olsa iki satır da olsa fiyatlar aynı çizgide kalıyor. items-center ve justify-between doğrudan align-items ve justify-content; Tailwind burada yeni bir şey öğretmiyor, yalnız yazımı kısaltıyor.

07-card-cart-tr.png

Flex item'ın boyutunu üç utility belirliyor ve birebir CSS'e karşılık geliyor:

node scripts/04-selectors.mjs 'flex-1' 'grow' 'shrink-0' 'basis-64'
flex-1
  .flex-1 {
    flex: 1;
  }

grow
  .grow {
    flex-grow: 1;
  }

shrink-0
  .shrink-0 {
    flex-shrink: 0;
  }

basis-64
  .basis-64 {
    flex-basis: calc(var(--spacing) * 64);
  }

flex-1, flex: 1 yani flex: 1 1 0% demek: item büyür, küçülür ve başlangıç boyutu sıfırdan hesaplanır. grow yalnız büyümeyi açıyor, başlangıç boyutu içerikten geliyor. Bu farkın neden önemli olduğunu CSS3 serisinin Flexbox yazısında flex-basis ile ölçmüştük. Başlığın iki satırdan sonra kesilmesini sağlayan line-clamp-2 bir typography utility'si; bir sonraki yazıda ona döneceğiz. Altındaki -webkit-line-clamp ve text-overflow mekanizması CSS Property'leri: Uygulamalı Rehber serisinin Satır Kırma ve Taşma yazısında.

Grid: Katalog Grid'i

Kartları dizen liste tek bir class'la responsive:

<ul id="catalog-grid" class="mt-6 grid grid-cols-[repeat(auto-fill,minmax(12rem,1fr))] gap-6">
grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]
  .grid-cols-\[repeat\(auto-fill\,minmax\(12rem\,1fr\)\)\] {
    grid-template-columns: repeat(auto-fill,minmax(12rem,1fr));
  }

Tailwind'in hazır grid-cols-3 utility'si repeat(3, minmax(0, 1fr)) üretiyor; kolon sayısı sabit. "Sığdığı kadar 12rem'lik kolon" için bir hazır utility yok, bu yüzden arbitrary value yazıyoruz. Köşeli parantez içinde boşluk yerine virgül kullanılıyor; boşluk gerekirse _ yazılıyor (grid-cols-[1fr_20rem]). Bu ifade sayfada bir kez geçiyorsa arbitrary value kabul edilebilir; birden fazla yerde tekrar ediyorsa onu @utility ile isimlendirmek daha iyi.

Playwright ile sayfayı üç genişlikte açtık:

npm run build:07
node scripts/07-layout.mjs
 375px  grid columns 1  toolbar column  count none   cart block  summary static  boxes 20, max diff vs plain CSS 0px
 800px  grid columns 3  toolbar row     count block  cart block  summary static  boxes 20, max diff vs plain CSS 0px
1280px  grid columns 5  toolbar row     count block  cart grid   summary sticky  boxes 20, max diff vs plain CSS 0px

Telefon genişliğinde tek kolon, tablette üç, masaüstünde beş kolon var; hiçbir breakpoint yazmadık. 1280px'de dört kitap için beş kolon olması bir ayrıntı: auto-fill boş kalan kolonu da yerinde tutuyor. Aynı değeri auto-fit ile değiştirip ölçtük:

4 books at 1280px: auto-fill card 230px, auto-fit card 294px

auto-fit boş kolonları sıfıra indiriyor ve dört kart satırı dolduracak kadar genişliyor. Katalogda filtre sonucu iki kitap kaldığında kartların dev boyuta çıkmasını istemediğimiz için auto-fill seçtik. auto-fill ile auto-fit arasındaki farkın ayrıntısı CSS3 serisinin CSS Grid 1 yazısında.

Grid'in geri kalanı da düz eşleme: col-span-2 → grid-column: span 2 / span 2, col-start-2 → grid-column-start: 2, row-span-2, grid-flow-dense, place-items-center. "Book of the week" kartına iki kolon vermek için col-span-2 yeterli.

Spacing ve Sizing: gap, space-* ve min-w-0

Boşluk için üç yol var. p-* ve m-* element'in kendi kutusuna yazıyor. gap-* flex ya da grid container'ının item'ları arasına yazıyor. space-x-* ve space-y-* ise child'lara margin veriyor ve v4'te selector'ı değişti:

space-x-4
  :where(.space-x-4 > :not(:last-child)) {
    --tw-space-x-reverse: 0;
    margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
    margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
  }

Margin, son child dışındaki her child'ın bitiş kenarına konuyor. v3'te ise kardeş combinator'ıyla her child'ın başına konuyordu; upgrade rehberi bu değişikliği büyük sayfalardaki performans sorunuyla açıklıyor ve sorun çıkarsa gap'e geçmeyi öneriyor. Tahmin sorusunun cevabı da burada.

Sizing tarafında sık kullanılanlar: size-* genişlik ve yüksekliği birlikte veriyor (ikon ve avatar için), w-screen 100vw, h-dvh 100dvh üretiyor; mobil tarayıcının adres çubuğuyla değişen yüksekliği için h-screen yerine h-dvh daha doğru. max-w-prose 65ch. Sepet satırındaki uzun başlık ise sizing'in en çok unutulan class'ını gösteriyor:

<li class="flex items-center gap-4 py-4">
    <div class="h-24 w-16 shrink-0 rounded bg-emerald-200"></div>
    <div class="min-w-0 flex-1">
        <h3 id="line-title" class="truncate font-medium">Middlemarch: A Study of Provincial Life (Penguin Classics, annotated edition)</h3>
        <p class="text-sm text-slate-600">Qty 1</p>
    </div>
    <span id="line-price" class="shrink-0 font-semibold">385.00 TRY</span>
</li>
cart line    title box 536px, text 571px

Başlığın kutusu 536px, metni 571px; truncate fazlasını üç noktayla kesiyor. Bunu mümkün kılan min-w-0: flex item'ın varsayılan minimum genişliği auto, yani içeriğinin genişliği. min-w-0 olmadan metni kesecek bir sınır yok. Kapak ve fiyattaki shrink-0 de kapağın 64px'ten, fiyatın tek satırdan aşağı ezilmesini engelliyor.

Position, inset ve z-index

Kapağın köşesindeki "New" etiketi, kapağa göre konumlanıyor:

node scripts/04-selectors.mjs 'inset-s-2' 'inset-x-0' 'z-10' 'aspect-2/3'
inset-s-2
  .inset-s-2 {
    inset-inline-start: calc(var(--spacing) * 2);
  }

inset-x-0
  .inset-x-0 {
    inset-inline: 0px;
  }

z-10
  .z-10 {
    z-index: 10;
  }

aspect-2/3
  .aspect-2\/3 {
    aspect-ratio: 2/3;
  }

Kapağın relative, etiketin absolute inset-s-2 top-2 olması yeterli. left-2 yerine inset-s-2 yazdık; bakım yazısında start-* utility'lerinin kullanımdan kalktığını ve yerine inset-s-* geldiğini görmüştük. Logical yazımın kazancını sağdan sola yazılan bir blokta ölçtük:

badge ltr    left 8px, right 179px
badge rtl    left 143px, right 8px

dir="rtl" olan blokta aynı class etiketi sağ köşeye taşıdı. ps-*, pe-*, ms-* ve me-* aynı şekilde çalışıyor. aspect-2/3 kapağın yüksekliğini genişliğinden türetiyor; görsel yüklenmeden önce yer ayrıldığı için CLS (Cumulative Layout Shift; sayfa yüklenirken içeriğin kayma miktarı) oluşmuyor. z-10 ise yalnız aynı stacking context içinde anlam taşıyor; z-50 yazıp bir modal'ın hâlâ altta kaldığını görüyorsan sorun sayıda değil, stacking context'te. Bunu CSS3 serisinin Normal Flow, display ve Positioning yazısında ayrıntılı ölçmüştük.

Sepet özetinde lg:sticky lg:top-4 var: geniş ekranda kullanıcı satırları kaydırırken "Checkout" butonu görünür kalıyor. Yukarıdaki çıktıda 1280px'de summary sticky, daha dar genişliklerde static.

Responsive Prefix'ler: Sepetin İki Kolonu

Breakpoint prefix'leri mobile-first: prefix'siz class her genişlikte, md: 48rem ve üstünde geçerli. Sepet sayfası telefonda tek kolon, 64rem'den itibaren iki kolon:

<section id="cart" class="mx-auto max-w-5xl px-4 py-8 lg:grid lg:grid-cols-[1fr_20rem] lg:items-start lg:gap-8">
    <ul id="cart-lines" class="divide-y divide-slate-200 rounded-lg bg-white px-4">...</ul>
    <aside id="summary" class="mt-8 rounded-lg bg-white p-6 lg:sticky lg:top-4 lg:mt-0">...</aside>
</section>

lg:items-start'ın ne işe yaradığını class'ı kaldırıp ölçerek gördük:

cart lines / summary height: with lg:items-start 257px / 160px
cart lines / summary height: without          257px / 257px

Grid item'ları varsayılan olarak satır yüksekliğine esniyor (align-items: stretch); class olmadan özet kutusu sepet satırları kadar uzadı. Böyle bir kutuda sticky işe yaramaz, çünkü kutu zaten satırın tamamını kaplıyor ve içinde kayacak yer kalmıyor. Telefonda özetin üstündeki mt-8 boşluğu, iki kolonda lg:mt-0 ile kaldırılıyor.

v4'te her breakpoint'in bir max-* karşılığı da var ve ikisi birleştirilince aralık oluyor:

max-md:hidden
  @media (width < 48rem) {
    .max-md\:hidden {
      display: none;
    }
  }

md:max-lg:grid-cols-2
  @media (width >= 48rem) {
    @media (width < 64rem) {
      .md\:max-lg\:grid-cols-2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }
  }

Katalogdaki "4 books" sayacı max-md:hidden ile telefonda gizleniyor (375px satırında count none). Burada hidden md:block da yazabilirdik; fark, max-md: ile element'in normal display değerini tekrar yazmak zorunda kalmamak. md:max-lg: yalnız tablet aralığına bir kural koymak için. Kendi breakpoint'in gerekiyorsa --breakpoint-* namespace'ine ekliyorsun; dokümanın uyarısı, bütün breakpoint'leri aynı birimle (rem) tanımlamak, yoksa üretilen kuralların sırası karışabiliyor.

Bir component sayfanın değil yerleştirildiği yuvanın genişliğine göre karar vermeliyse viewport breakpoint'i yanlış araç; @container ve @md: gibi container variant'larını variant yazısında ölçmüştük. Kitap kartı ana sayfadaki dar bir yan kolona da girecekse onun iç düzeni container query'ye, sayfa iskeleti viewport breakpoint'lerine bırakılmalı.

Aynı Ekran Düz CSS ile

Utility'lerin gerçekten "sadece CSS" olduğunu görmek için aynı katalog ve sepet sayfasını semantic class'lar ve elle yazılmış bir stylesheet'le kurduk. Kartın ve sepetin düz CSS karşılığı:

.book-card { display: flex; flex-direction: column; height: 100%; overflow: hidden; border: 1px solid #e2e8f0; border-radius: 0.5rem; background: #fff; }
.book-card__cover { position: relative; aspect-ratio: 2 / 3; }
.book-card__body { display: flex; flex: 1; flex-direction: column; gap: 0.25rem; padding: 1rem; }
.book-card__footer { margin-top: auto; display: flex; align-items: center; justify-content: space-between; padding-top: 0.75rem; }

.cart { max-width: 64rem; margin-inline: auto; padding: 2rem 1rem; }
.cart-summary { margin-top: 2rem; border-radius: 0.5rem; background: #fff; padding: 1.5rem; }
@media (width >= 64rem) {
    .cart { display: grid; grid-template-columns: 1fr 20rem; align-items: start; gap: 2rem; }
    .cart-summary { position: sticky; top: 1rem; margin-top: 0; }
}

07-layout.mjs iki sayfada 20 kutunun (toolbar, kartlar, kapaklar, butonlar, sepet kolonları, başlık ve fiyat) konumunu ve boyutunu karşılaştırıyor; yukarıdaki çıktının son sütununda üç genişlikte de en büyük fark 0px. İki yazım aynı düzeni üretiyor. Farklı olan, kararın nerede durduğu. Düz CSS'te "sepet 64rem'de iki kolon olur" bilgisi stylesheet'te, HTML'de yalnız cart adı var. Tailwind'de bilgi template'te ve okuyunca görünüyor; karşılığında bir class listesi uzuyor. İlk yazıda bu takası ölçmüştük; burada eklenecek tek şey, layout utility'lerinin bire bir eşleme olduğu için düz CSS bilgisinin aynen geçerli kalması. flex-basis'i ya da auto-fill'i bilmeyen biri Tailwind'de de doğru class'ı seçemiyor.

Üretimde Dikkat

Layout utility'leri büyük ölçüde değişmez değerler üretiyor, bu yüzden CSS boyutunu artıran onlar değil, variant kombinasyonları. 07/index.out.css bu sayfa için minify edilmeden 11.8 kB, --minify ile 9.7 kB; bunun yarısından azı (5.6 kB) utility'ler, kalanı Preflight ve kullanılan theme değişkenleri.

Ekran genişliğine göre değişen düzeni tek genişlikte test etme. 07-layout.mjs'deki gibi üç genişlikte açıp kolon sayısını, display ve position değerlerini okuyan küçük bir Playwright testi, birinin lg:items-start'ı "gereksiz" diye silmesini hemen yakalar.

Dinamik class'larda kurulum yazısındaki kural burada da geçerli: 'grid-cols-' + count gibi birleştirilmiş adlar taranmıyor ve üretilmiyor. Kolon sayısı veriden geliyorsa tam class adlarını bir map'te tut ({ 2: "grid-cols-2", 3: "grid-cols-3" }) ya da değeri style="--cols: 3" ile verip grid-cols-[repeat(var(--cols),minmax(0,1fr))] ile oku. Kısa yazım grid-cols-(--cols) burada işe yaramaz: grid-template-columns: var(--cols) üretiyor, yani değişkenin içinde 3 değil tam bir track listesi olmalı.

Sık Düşülen Tuzaklar

İlki tahmindeki tuzak. Etiket kutusunu ölçtük:

#chips-space rows 2, vertical gap 0px, row 1 space left 39px / right 55px
#chips-gap   rows 2, vertical gap 16px, row 1 space left 47px / right 47px

space-x-4 ile iki satır birbirine yapışıyor, çünkü yalnız yatay margin veriyor. İlk satır da tam ortada değil: satırın son etiketi aslında listenin son child'ı olmadığı için bitiş margin'ini taşıyor ve satır 16px sola kayıyor. Aynı kutuda gap-4 hem satırların arasına 16px koyuyor hem satırı ortalıyor. Wrap olan her yerde gap-* kullan; space-* yalnız tek satırlı, sırası değişmeyen listelerde güvenli.

İkincisi uzun başlıkta min-w-0'ı unutmak. Aynı satırı min-w-0 olmadan kurduk:

#row-no-min  container 320px, price right edge 677px

320px'lik satırda fiyat 677px'e itildi; truncate hiçbir şey kesmedi, çünkü başlığın kutusu metinle birlikte büyüdü. Flex ya da grid içindeki metin kesilmiyorsa ilk bakılacak yer min-w-0 (grid'de minmax(0, 1fr)).

Üçüncüsü her değeri arbitrary value ile yazıp ölçekten çıkmak. Layout değerlerinde canonicalize komutunu denedik: gap-[1.5rem] → gap-6, h-[18rem] → h-72, max-w-[80rem] → max-w-7xl, p-[3.25rem] → p-13, aspect-[2/3] → aspect-2/3. gap-[24px] ve w-[288px] değişmedi (piksel değerleri rem'e çevrilmiyor). İlginç olanı w-[18rem]: h-[18rem] h-72 olurken bu olduğu gibi kaldı. 18rem hem w-72'ye hem container ölçeğindeki w-2xs'e eşit; araç iki aday arasında seçim yapmamış görünüyor. Kural yine aynı: değer ölçekte varsa ölçekten yaz, yoksa bunun bir token mı yoksa bir kerelik bir ayar mı olduğuna karar ver.

Dördüncüsü z-* değerlerini sayı yarışına çevirmek. z-[9999] gördüğün yerde katmanların listesini çıkar (header, dropdown, modal, toast) ve bunları birkaç sabit değere bağla.

Küçük Bir Deneme

★☆☆ 07/index.html'deki #chips-space'i gap-x-4 gap-y-2 ile değiştir, npm run build:07 ve node scripts/07-layout.mjs çalıştır ve dikey boşluğun 8px olduğunu doğrula.

★★☆ Katalog grid'ine col-span-2 alan bir "Book of the week" kartı ekle. 375px'de tek kolonlu grid'de ne olduğunu ölç, sonra sm:col-span-2 ile düzelt ve 07/plain.html'e aynı kuralı düz CSS ile yaz; script'in 0px farkı koruduğunu gör.

★★★ Sepet sayfasının iki kolonlu düzenini @container ile yeniden yaz: #cart'ı bir container yap ve kolon değişimini @3xl: gibi bir container variant'ına bağla. Sayfayı 1280px'de 600px'lik bir yan panele koyduğunda iki çözümün farklı davrandığını Playwright ile göster.

Bir sonraki yazıda serinin son bölümüne, görünüm tarafındaki utility ailelerine geçiyoruz: typography, renk ve opacity modifier'ı, border ve shadow, effect ve transform, form utility'leri, sonunda da CMS'ten (içerik yönetim sistemi) gelen blog gövdesi için @tailwindcss/typography ve form'lar için @tailwindcss/forms.