Mert Özen Her Satırda Daha İleri
Frontend Sass/SCSS — #06

Büyük Projede Stil Mimarisi: 7-1 Pattern, BEM, ITCSS ve Cascade Layers ile Katmanlı CSS

Mert Özen 14 Kas 2026 9 dk 12 görüntülenme
Büyük Projede Stil Mimarisi: 7-1 Pattern, BEM, ITCSS ve Cascade Layers ile Katmanlı CSS

Büyük bir stil codebase'i CSS üretmeyen bir abstracts ile CSS üreten klasörlere böl, isimleri BEM ile düz tut ve ITCSS'in genelden özele sırasını @layer ile tarayıcıya taşı; Sass'ta katmanlar @use ile değil meta.load-css() ile kurulur. Bookstore'un stil ağacını 7-1'e çeviriyor, vendor CSS'i katmana alıyor ve Angular component stillerinin katmansız olduğu için en uzun katmanlı selector'ü bile yendiğini ölçüyoruz.

Sürüm notu: Bu yazıdaki örnekler Dart Sass 1.108.2, Angular 22.2 (TypeScript 6.0; Angular CLI'ın içindeki Sass 1.104.1), Angular Material 22.2.7, modern-normalize 3.0.1 ve Playwright 1.65'in Chromium build'iyle denenmiştir.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'a (bir online kitapçı) teslimat tarihi seçimi eklenirken üçüncü parti bir datepicker'ın CSS'i projeye girdi. Seçili günü Bookstore'un marka rengiyle göstermek için BEM (Block, Element, Modifier; class isimlendirme yöntemi) kuralına uygun, tek class'lı bir kural yazıldı:

// üçüncü parti datepicker CSS'i: uzun selector'ler, yüksek specificity
.datepicker table td.is-selected {
    background-color: #2563eb;
    color: #ffffff;
}

// Bookstore'un teslimat tarihi seçicisi: tek class, BEM
.calendar-day--selected {
    background-color: #b45309;
    color: #ffffff;
}

Seçili gün mavi mi olur, kahverengi mi? Bizim kuralımız sonra geliyor ama specificity'si (0,1,0), datepicker'ınki (0,2,2). Peki aynı iki dosyayı @layer vendors, components; ile iki katmana koyarsak?

node scripts/06-layers.mjs
06/main-unlayered.scss selected day background=rgb(37, 99, 235)
06/main-layered.scss   selected day background=rgb(180, 83, 9)

Katmansız hâlde datepicker kazanıyor: specificity kaynak sırasından önce geliyor. Katmanlı hâlde bizim kuralımız kazanıyor: layer sırası specificity'den önce geliyor. Bu yazının konusu bu iki satırın arasındaki fark: büyük bir projede stil kodunu klasörlere, isimlere ve katmanlara nasıl böldüğümüz ve Sass'ın bunu kurmaktaki rolü. Cascade'in sırasının kendisi CSS3 serisinin ilk yazısında anlatıldı.

7-1 Klasör Yapısı

7-1 pattern, Kitty Giraudel'in Sass Guidelines'ında önerdiği bir klasör düzeni: yedi klasör ve hepsini bir araya getiren tek bir giriş dosyası. Klasörler abstracts, vendors, base, layout, components, pages ve themes. Rehber @import döneminde yazıldı; fikri module sistemine taşımak için tek bir kural yeterli: CSS üreten klasörler abstracts'ı @use eder, abstracts hiçbir şeyi.

Bookstore'un Angular istemcisi bookstore-web'de bu yapıyı biraz değiştirerek kurdum. pages yok, çünkü Angular'da her sayfa zaten bir component ve kendi stil dosyası var. Onun yerine ITCSS'ten (Inverted Triangle CSS; stilleri genelden özele katmanlara ayıran yöntem) gelen bir utilities klasörü var:

src/
  styles.scss                 // katman sırası + meta.load-css()
  styles/
    abstracts/                // CSS üretmez
      _index.scss             // @forward "tokens", "breakpoints", "theme"
      _tokens.scss            // $themes, $radius, $space
      _breakpoints.scss       // $breakpoints, from()
      _theme.scss             // color-tokens()
    vendors/_index.scss       // modern-normalize
    themes/
      _index.scss             // mat.theme(), color-tokens(), color-scheme
      _theme-colors.scss      // ng generate ile üretilmiş palet
    base/_typography.scss     // body, h1–h3
    layout/_page.scss         // .page
    components/_stock-badge.scss
    utilities/_spacing.scss   // .m-0 … .my-4
  app/
    book-card/book-card.scss  // component stili (Angular)

Her CSS üreten klasörün bir _index.scss'i var ve içindeki dosyaları @use ile yüklüyor; abstracts'taki index ise @forward kullanıyor, çünkü onun işi CSS üretmek değil, değişken ve mixin'leri dışarı açmak. Fark önemli: @use edilen bir dosyanın CSS'i çıktıya girer, @forward edilen bir dosyanın üyeleri dışarı açılır.

Yapının özeti aşağıdaki görselde: solda klasörler ve bağımlılık yönü, sağda bu klasörlerin tarayıcıda hangi sırayla değerlendirildiği.

06-style-tree-tr.png

BEM: Specificity'yi Düz Tutmak

BEM bir klasör yapısı değil, bir isimlendirme kuralı. Her bağımsız UI parçası bir block (.book-card), onun parçaları element (.book-card__title), durumları ve varyantları modifier (.book-card--featured). Sonuç, her selector'ün tek bir class olması ve specificity'nin her yerde (0,1,0) kalması. İkinci yazıda gördüğümüz derin nesting sorunu BEM'de zaten ortaya çıkmıyor; Sass'ın &__title ve &--featured yazımı da BEM'i yazmayı kolaylaştırıyor.

Bookstore'da iki kuralı daha uyguluyoruz. Block'lar iç içe geçebilir ama birbirinin element'ini stillemez: .book-card'ın içindeki .stock-badge'in rengini .book-card .stock-badge ile değiştirmek yerine badge'e bir modifier ekliyoruz. İkincisi, element'in element'i yok: .book-card__price__currency yerine .book-card__currency. Bu kurallar DOM (sayfanın element ağacı) değiştiğinde stillerin kırılmasını engelliyor.

Angular'da BEM'in bir kısmı gereksiz görünebilir: emulated encapsulation zaten component'in selector'lerini kendi template'ine sınırlıyor. Yine de BEM'i component içinde de kullanıyoruz; çünkü .title gibi genel bir class adı, aynı adı kullanan global bir kuralla çakışabiliyor ve DevTools'ta hangi component'e ait olduğunu söylemiyor.

ITCSS'in Katman Fikri ve @layer

ITCSS, Harry Roberts'ın stil dosyalarını etki alanı genişten dara ve specificity düşükten yükseğe doğru sıralama önerisi: settings, tools, generic, elements, objects, components, utilities. İlk ikisi CSS üretmiyor (Sass'ta abstracts'ın karşılığı), sonrakiler sırayla daha az element'i ama daha güçlü etkiliyor.

ITCSS'in yıllarca bir sorunu vardı: sıralama yalnız bir sözleşmeydi. Bir vendor dosyasındaki uzun bir selector, sonra gelen bir component kuralını specificity ile yenebiliyordu; yazının başındaki datepicker örneği tam olarak bu. Cascade layers bu sözleşmeyi tarayıcıya taşıdı. Katman sırası specificity'den önce değerlendirildiği için, sonraki katmandaki tek class'lı bir kural önceki katmandaki uzun bir selector'ü her zaman yeniyor. Bookstore'da ITCSS katmanlarını şöyle eşledim:

ITCSSBookstore klasörü@layer
Settings, Toolsabstracts/yok (CSS üretmez)
Genericvendors/vendors
(tema değişkenleri)themes/themes
Elementsbase/base
Objectslayout/layout
Componentscomponents/ ve Angular component'lericomponents / katmansız
Utilities (Trumps)utilities/utilities

Sass ile Katmanları Kurmak: meta.load-css()

İlk akla gelen, @use'u bir @layer bloğunun içine yazmak. Sass buna izin vermiyor:

@layer vendors {
    @use "vendors/datepicker";
}
Error: This at-rule is not allowed here.
  ╷
2 │     @use "vendors/datepicker";
  │     ^^^^^^^^^^^^^^^^^^^^^^^^^
  ╵
  06/use-in-layer.scss 2:5  root stylesheet

@use yalnız dosyanın en üst seviyesinde yazılabiliyor. Bir module'ün CSS'ini istediğin yere yerleştirmenin yolu sass:meta module'ündeki load-css() mixin'i. Bookstore'un giriş dosyası artık yalnız bundan ibaret:

@use "sass:meta";

// katman sırasına bir kez, burada karar verilir: specificity ne olursa olsun sonraki katman öncekini yener
@layer vendors, themes, base, layout, components, utilities;

@layer vendors {
    @include meta.load-css("vendors");
}

@layer themes {
    @include meta.load-css("themes");
}

@layer base {
    @include meta.load-css("base");
}

@layer layout {
    @include meta.load-css("layout");
}

@layer components {
    @include meta.load-css("components");
}

@layer utilities {
    @include meta.load-css("utilities");
}

İlk satırdaki @layer listesi önemli: katmanların sırası, tarayıcının onları ilk gördüğü sıraya göre belirleniyor. Listeyi en başa koymak, bir dosyanın yeri değişse bile sıranın değişmemesini sağlıyor. Vendor klasörü bir npm paketinin CSS'ini yüklüyor; Sass düz .css dosyalarını da module olarak yükleyebiliyor:

// üçüncü parti CSS, katmana konabilsin diye Sass module'ü olarak yükleniyor
@use "modern-normalize/modern-normalize";

Build'den sonra global stil dosyasında katmanların nasıl çıktığına bakmak için compressed CSS'te her katmanın ilk karakterlerini yazdıran küçük bir script çalıştırdım:

node scripts/06-layer-dump.mjs
@layer vendors,themes,base,layout,components,utilities;
vendors -> *,:before,:after{box-sizing:border-box}html{font-family:system-ui,Segoe UI,Roboto,Helvetic
themes -> html{color-scheme:light dark;--mat-sys-background: light-dark(#fff8f6, #1b110c);--mat-sys-
base -> body{margin:0;font-family:system-ui,sans-serif;line-height:1.5;background-color:var(--colo
layout -> .page{max-width:80rem;margin-inline:auto}@media(width>=64rem){.page{padding-inline:16px}}}
components -> .stock-badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:.75rem}.st
utilities -> .m-0{margin:0}.m-1{margin:4px}.m-2{margin:8px}.m-3{margin:16px}.m-4{margin:24px}.mt-0{marg

Altı katman, beklenen sırada. Bu yeniden düzenlemeden sonra önceki yazının tema ölçümünü (05-theme.mjs) ve badge ölçümlerini tekrar çalıştırdım; bütün değerler aynı çıktı.

Bookstore'da Global, Component ve Utility Sınırı

Angular projesinde stil kodu üç yerde yaşıyor ve hangisinin nereye gideceği en çok tartışılan konu. Bookstore'daki kural:

  • Component stili: yalnız bir Angular component'inin template'inde kullanılan her şey, o component'in .scss dosyasında. BookCard'ın kenarlığı, başlığı, fiyatı.
  • Global component: birden fazla Angular component'inin template'inde kullanılan ve kendi davranışı olmayan küçük UI parçaları styles/components/'ta. Stok badge'i katalog kartında, kitap sayfasında ve sepette görünüyor; üçüncü yazıda BookCard'ın içinde ürettiğimiz badge class'larını buraya taşıdım.
  • Utility: tek bir property'yi değiştiren, her yerde kullanılabilen class'lar styles/utilities/'te ve en güçlü katmanda.

Angular component stilleri hiçbir katmanın içinde değil. Angular onları component'e özel selector'lerle (.book-card__title[_ngcontent-…]) sayfaya ekliyor ve katmansız (unlayered) stiller bütün katmanlı stillerden güçlü. Bunu yerleşik uygulamada ölçtüm: önce en güçlü katmana (0,3,1) specificity'li bir kural, sonra katmansız ama (0,2,1) specificity'li bir kural ekleyip kitap başlığının rengini okudum:

node scripts/06-web-layers.mjs
layer order: vendors < themes < base < layout < components < utilities
layered   (0,3,1) global rule → title color rgb(180, 83, 9)
unlayered (0,2,1) global rule → title color rgb(220, 38, 38)

utilities katmanındaki uzun selector bile component stilini değiştiremedi; katmansız kural ise yalnız specificity'si component'inkinden (0,2,0) yüksek olduğu için kazandı. Bu davranış bizim istediğimiz şey: global stiller hiçbir zaman bir component'in kendi stilini kazara ezemiyor. Angular Material'ın component stilleri de katmansız, bu yüzden base'e yazılan bir button kuralı Material butonlarını bozmuyor. Denedim: base katmanına button { border-radius: 0; background-color: … } ekleyince "Add to cart" butonu 8px köşesini ve rgb(155, 69, 0) arka planını korudu. Bedeli, utility class'larının da component stillerini ezememesi; class="m-0"'ı bir component'in kendi margin'ini tanımladığı element'te kullanırsan component kazanıyor. Bookstore'da utility'leri yalnız component stilinin dokunmadığı property'ler için kullanıyoruz.

Utility-first ile Karşılaştırma

Bu yazıdaki yaklaşım "semantik class + az sayıda utility". Tailwind gibi utility-first bir framework ise tersini yapıyor: stilin neredeyse tamamı template'te utility class'larıyla yazılıyor, CSS dosyası neredeyse boş kalıyor. Tailwind v4 kendi içinde theme, base, components ve utilities adlı cascade layer'ları kullanıyor; yani katman fikri orada da aynı. Fark, kararın nerede verildiği: bizde class adı bir anlam taşıyor (.stock-badge--low) ve görünüm CSS'te; utility-first'te görünüm template'te. İki yaklaşımın karşılaştırması ve ne zaman hangisinin seçileceği Tailwind CSS serisinin konusu. Bir projede ikisini birlikte kullanmamak için ilk yazıdaki sebepler hâlâ geçerli.

Üretimde Dikkat

Katman sırasını tek yerde tut. @layer listesi yalnız giriş dosyasının en başında olmalı. Başka bir dosyada yeni bir katman adı ilk kez kullanılırsa tarayıcı onu o anki sıraya ekliyor ve hangi katmanın kazandığı dosyaların yükleme sırasına bağlı hâle geliyor; tam da kaçmak istediğimiz şey.

!important'ın katmanlarda ters çalıştığını unutma. Normal declaration'larda sonraki katman kazanıyor; !important declaration'larda önceki katman kazanıyor. Denedim: vendors ve utilities katmanında aynı property'ye !important verdiğimde vendors'daki değer kazandı. Vendor CSS'inde !important varsa onu katmana almak, onu daha da güçlü yapıyor.

Global CSS'in boyutunu katman bazında izle. Bookstore'da global stil dosyası 10,65 kB ve bunun büyük kısmı themes katmanındaki Material token'ları. Hangi katmanın büyüdüğünü bilmek, bir sonraki "CSS neden büyüdü?" sorusunu kısaltıyor.

Sık Düşülen Tuzaklar

7-1'i küçük projeye dayatmak. İki yüz satırlık bir stil kodu için yedi klasör, her birinde bir _index.scss ve tek dosya, aramayı ve okumayı zorlaştırıyor. Bookstore'un bugünkü hâli bile sınırda; bazı klasörlerde tek dosya var. Ben yapıyı stil kodu büyüdükçe açıyorum: önce abstracts ve tek bir global dosya, sonra gerçekten ayrışan kısımlar için klasörler.

pages klasörünün çöplüğe dönmesi. Klasik 7-1'de pages/_checkout.scss gibi dosyalar zamanla "nereye koyacağımı bilmediğim her şey"in adresi oluyor ve bir sayfanın stilleri başka sayfalara sızıyor. Angular'da bu klasöre gerek yok; sayfaya özel stil, sayfa component'inde.

Katmansız global CSS bırakmak. Katmanlara geçerken bir dosyayı katmana koymayı unutursan, o dosyadaki her kural bütün katmanları yeniyor ve üstelik bunu sessizce yapıyor. Bookstore'daki kural: giriş dosyasında sass:meta ve katman blokları dışında hiçbir @use ya da CSS kuralı olmaz; giriş dosyasına dokunan bir değişiklikte code review'da ilk bakılacak şey bu.

Katmanları specificity sorununa tek çözüm sanmak. Katmanlar katmanlar arasındaki çatışmayı çözüyor; aynı katmanın içinde specificity ve kaynak sırası hâlâ geçerli. components katmanında derin nesting'le yazılmış bir kural, aynı katmandaki BEM kuralını yine ezer.

Küçük Bir Deneme

★☆☆ 06/main-layered.scss'teki @layer vendors, components; satırını sil ve iki katman bloğunun yerini değiştir. node scripts/06-layers.mjs çıktısı nasıl değişiyor ve neden?

★★☆ bookstore-web'e bir styles/components/_price.scss ekle (indirimli fiyat için .price--sale) ve BookCard'da kullan. Kuralın component'in kendi &__price stiliyle çatıştığı bir property seç ve web-probe.mjs ile hangi kuralın kazandığını, sebebiyle birlikte göster.

★★★ @angular/cdk'nın overlay-prebuilt.css dosyasını vendors katmanına al ve bir Material dialog'u açan küçük bir sayfa ekle. Dialog'un konumlandırmasının katmana alındıktan sonra da doğru çalıştığını Playwright ile doğrula; çalışmıyorsa sebebini katman sırasıyla açıkla.

Bir sonraki yazının konusu Dart Sass 2.0'a hazırlık. Hangi deprecation'ların hataya dönüşeceği, --fatal-deprecations ile bugünden hata almak, sass-migrator ile slash division'ı temizlemek ve Angular'ın kendi Sass sürümüyle bu ayarları nasıl vereceğimiz.