Mert Özen Her Satırda Daha İleri
Frontend CSS3: Flexbox, Grid ve Modern CSS — #12

Native CSS Nesting ve @scope: Preprocessor'süz Component Stilleri

Mert Özen 14 Kas 2026 15 dk 0 görüntülenme
Native CSS Nesting ve @scope: Preprocessor'süz Component Stilleri

Native CSS nesting yazımı kısaltır ama hiçbir şeyi izole etmez; @scope ise bir kuralın scope root'tan limit'e kadar geçerli olmasını sağlar ve donut scope ile kullanıcı içeriğini dışarıda bırakır. Bookstore kartını nesting ile yeniden yazıp computed style hash'iyle değişmediğini kanıtlıyor, &'nin Sass'tan farkını, :is() kaynaklı specificity'yi, proximity'yi ve Angular, ASP.NET Core CSS isolation ve Shadow DOM karşılaştırmasını ölçüyoruz.

Sürüm notu: Bu yazı CSS Nesting Module ve CSS Cascading Level 6 (@scope) 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 yıllarca Sass yazmış bir ekip arkadaşımız, native CSS nesting'in tarayıcılara geldiğini duyunca kart stillerini alışkanlıkla şöyle yazdı:

.book-card {
    &__title { color: rgb(185, 28, 28); }
}

Sass'ta bu .book-card__title üretir ve BEM (Block, Element, Modifier; class isimlendirme yöntemi) ile yazan herkesin her gün kullandığı bir kısayoldur. Aynı satırı hiçbir derleme adımı olmadan doğrudan tarayıcıya verdiğimizde <h3 class="book-card__title"> kırmızı olur mu? Olmazsa tarayıcı bu satırı ne olarak anlar?

Bu yazıda bir preprocessor'a ihtiyaç duymadan component stili yazmanın iki aracına bakıyoruz. Native nesting yazımı kısaltıyor ama hiçbir şeyi izole etmiyor; @scope ise bir kuralın nereden nereye kadar geçerli olduğunu söylüyor. İkisini Bookstore'un kitap kartında kullanacak, sonra Angular'ın emulated encapsulation'ı, ASP.NET Core'un CSS isolation'ı ve Shadow DOM (bir element'e bağlı, sayfanın stillerinden ayrı tutulan element ağacı) ile karşılaştıracağız.

Native Nesting: Bookstore Kartını İç İçe Yazmak

Kitap kartının stilleri serinin altı yazısı boyunca büyüdü: subgrid, kapaksız kart, stok durumu, üç tür container query. Hepsi .book-card ile başlayan ayrı kurallardı. Dosyayı nesting ile yeniden yazdım:

.book-list {
    display: grid;
    /* sığdığı kadar en az 14rem'lik kolon; min() çok dar ekranda tek kolonun taşmasını önler */
    grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
    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;
}

/* 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);

    & > * {
        margin: 0;
    }

    & > img {
        grid-area: cover;
        justify-self: center;
    }

    /* & gerekmez: iç içe ".book-title", ".book-card .book-title" demektir */
    .book-title {
        grid-area: title;
    }

    .book-author {
        grid-area: author;
    }

    /* fiyat satırı ortak; fiyat satırın alt kenarına oturur */
    .price {
        grid-area: price;
        align-self: end;
    }

    /* kapak görseli olmayan kart onun yerine renkli bir zemin alır */
    &:not(:has(> img)) {
        background-color: var(--color-tint);
    }

    /* zebra çizgileri yalnız stokta olan kitapları sayar */
    &:nth-child(even of .in-stock) {
        border-color: light-dark(#93c5fd, #1e40af);
    }

    &[data-stock="0"] .price {
        text-decoration: line-through;
        color: var(--color-muted);
    }

    /* editörün notu bütün kart boyunca, her şeyin altında durur */
    .staff-note {
        grid-column: 1 / -1;
    }

    &[data-format~="ebook"] .book-title::after {
        content: " · e-book";
        font-size: 0.75em;
        color: var(--color-muted);
    }

    /* geniş yuva: kapak solda, metin sağda */
    @container book-card (width >= 32rem) {
        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-title {
            font-size: clamp(1.25rem, 1rem + 1.5cqi, 2rem);
        }
    }

    /* name-only query: sidebar'daki her kartın kapağı daha küçük */
    @container sidebar {
        & > img {
            inline-size: 4rem;
            block-size: auto;
        }
    }

    /* style query: yuva koyu olduğunu söyler, kart ona uyar */
    @container style(--surface: dark) {
        border-color: #4b5563;
        background-color: #111827;
        color: #f9fafb;

        .book-author {
            color: #d1d5db;
        }
    }
}

.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;

    /* accent ile hangisi daha çok kontrast veriyorsa siyah ya da beyaz; yukarıdaki #ffffff eski tarayıcılar için kalır */
    @supports (color: contrast-color(red)) {
        color: contrast-color(var(--color-accent));
    }
}

/* kartın içinde bağlantılar düz metin gibi görünür (başlık bir bağlantı), ama yalnız editörün notuna kadar:
   .user-content'teki bağlantılar CMS'ten gelir ve alt çizgilerini korur */
@scope (.book-card) to (.user-content) {
    a {
        color: inherit;
        text-decoration: none;
    }
}

İç içe yazılan bir kural, ebeveyninin selector'ünü kendiliğinden alır. & ebeveyni temsil eder: &:not(:has(> img)), .book-card:not(:has(> img)) demek. 2023 sonundan beri tarayıcılar gevşetilmiş (relaxed) syntax'ı da destekliyor: iç kural bir class ya da element adıyla başlıyorsa & yazmak gerekmiyor; .book-title { … }, .book-card .book-title demek. Container query'ler ve @supports de kuralın içine girebiliyor; içlerindeki declaration'lar doğrudan ebeveyne uygulanıyor. Kartın bütün davranışı artık tek bir blokta ve bir sonraki geliştirici "kart dar yuvada nasıl görünüyor?" sorusunun cevabını başka bir dosyada aramıyor.

Bir dosyayı baştan yazmak, hiçbir şeyin değişmediğini kanıtlamayı gerektirir. Bunun için katalog listesindeki ve sol kolondaki bütün element'lerin bütün computed property'lerini toplayıp hash'leyen küçük bir script yazdım. Aynı script'i bir önceki yazının etiketine (11) kopyalayıp çalıştırdım:

chromium 1280px light 26 elements, styles 50ea69a2d320dc35
chromium  360px dark  26 elements, styles 8c2e7baa93a5834a
webkit   1280px light 26 elements, styles cceaae2760f0c139
webkit    360px dark  26 elements, styles 2929edf0bb07047d

Nesting'li dosyayla:

node scripts/12-fingerprint.mjs
chromium 1280px light 26 elements, styles 50ea69a2d320dc35
chromium  360px dark  26 elements, styles 8c2e7baa93a5834a
webkit   1280px light 26 elements, styles cceaae2760f0c139
webkit    360px dark  26 elements, styles 2929edf0bb07047d

İki motorda, iki genişlikte, açık ve koyu temada hash'ler birebir aynı. Bu tür bir refactoring'de görsel karşılaştırma aracına gerek kalmadan "sayfa değişmedi" diyebilmek için bu yöntem yeterli. Script'i yazarken bir şey öğrendim: Chromium computed style'daki custom property'leri sabit bir sırada listelemiyor ve sıralamadan hash'lenince sonuç çalışmadan çalışmaya değişebiliyor; bu yüzden property'leri hash'lemeden önce sıralıyorum.

& Sass'taki & Değil

Native nesting Sass'a benziyor ama iki önemli farkı var. Lab sayfasında ikisini de ölçtüm:

/* 1) & metin değil, selector'dür: bu __title.book-card demek, .book-card__title değil */
.book-card {
    &__title { color: rgb(185, 28, 28); }
}

/* 2) ebeveyn selector listesi :is() içine alınır, :is() de en yüksek specificity'yi alır */
#featured, .book-card {
    & .price { font-weight: 700; }
}
.book-card .price { font-weight: 400; }

/* 3) her nesting seviyesi ebeveynin specificity'sini ekler */
.catalog {
    .book-list {
        .book-card {
            .book-title { color: rgb(29, 78, 216); }
        }
    }
}
.book-title.highlight { color: rgb(22, 163, 74); }
node scripts/12-nesting.mjs
node scripts/12-scope.mjs
--- .book-card__title
computed color: rgb(0, 0, 0)
--- .book-card .price
computed font-weight: 700
✓ (unlayered)        & .price                             (1,1,0)  700                    12-nesting.html
✗ (unlayered)        .book-card .price                    (0,2,0)  400                    12-nesting.html
--- .highlight
computed color: rgb(29, 78, 216)
✓ (unlayered)        .book-title                          (0,4,0)  rgb(29, 78, 216)       12-nesting.html
✗ (unlayered)        .book-title.highlight                (0,2,0)  rgb(22, 163, 74)       12-nesting.html
--- webkit: .book-card__title rgb(0, 0, 0), .price 700, .highlight rgb(29, 78, 216)

İlk blok tahmin sorusunun cevabı: .book-card__title için tek bir kural bile eşleşmedi ve başlık body'den inherit edilen siyahla kaldı. Sass'ta & bir metin parçası gibi davranır ve ardından gelen __title ile birleşir. Native CSS'te & bir selector'dür; &__title, __title adında bir element ile .book-card class'ının birleşimi olarak okunur. Böyle bir element olmadığı için kural hiçbir şeyi seçmez ve hiçbir hata da vermez. BEM isimlerini native nesting ile kısaltamazsın; tam class adını yazman gerekir.

İkinci blok daha sinsi. #featured, .book-card { & .price { … } } yazınca tarayıcı ebeveyn listesini :is() içine alıyor: kural :is(#featured, .book-card) .price oluyor. :is() ise, serinin selector yazısında gördüğümüz gibi, listesindeki en yüksek specificity'yi alır. Bu yüzden kural yalnız .book-card içindeki bir fiyatı seçerken bile specificity'si (1,1,0); DevTools'un da gösterdiği değer bu. Sonradan yazılan .book-card .price (0,2,0) kaybediyor ve fiyat kalın kalıyor. Sass aynı kodu #featured .price, .book-card .price diye iki ayrı selector'e açardı ve sorun çıkmazdı. Ebeveyn listesinde bir id varsa nesting'i bölmek en güvenli yol.

Derin Nesting ve Specificity Şişmesi

Lab'deki üçüncü örnek dört seviye iç içe yazılmış: .catalog .book-list .book-card .book-title. Specificity (0,4,0). Bileşenin kendi dosyasında daha sonra yazılmış .book-title.highlight (0,2,0) bu yüzden kaybediyor ve vurgu rengi hiç görünmüyor. İç içe yazım, HTML'in yapısını CSS'e kopyalamayı çok kolaylaştırıyor; her seviye bir class daha ekliyor ve sonunda bir kuralı ezmek için daha da uzun bir selector gerekiyor. Bu, serinin ilk yazısında anlattığımız "specificity savaşı"nın nesting'li hali.

Benim kuralım şu: nesting'i bir component'in kendi parçaları ve durumları (&:hover, &[data-stock="0"], container query) için kullan, sayfanın yapısını taklit etmek için değil. Kitap kartında en derin seviye iki: .book-card ve onun bir parçası. Bir kuralın specificity'si bir class'tan fazla olmamalıysa :where() ya da bir sonraki bölümdeki @scope daha doğru araç.

@scope: Scope Root, Limit ve Donut Scope

Nesting yazımı kolaylaştırır ama bir kuralın nereye kadar ulaşacağını sınırlamaz. Bookstore'da bunun somut bir örneği oldu. Haftanın kitabı kartında başlık artık bir bağlantı ve kartın içindeki bağlantıların düz metin gibi görünmesini istiyoruz. Aynı kartta editörlerin CMS'te (Content Management System; içerik yönetim sistemi) yazdığı kısa bir not da var ve notun içindeki bağlantılar bağlantı gibi görünmeli:

<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"><a href="book.html">Dune</a></h3>
    <span class="badge">Bestseller</span>
    <p class="book-author">Frank Herbert</p>
    <p class="price">349.00 TRY</p>
    <div class="staff-note user-content">
        <p>Our pick of the week. Read the <a href="guide.html">reading guide</a> first.</p>
    </div>
</article>

.book-card a { text-decoration: none; } yazarsam kural notun içindeki bağlantıya da ulaşır. Lab'de iki yolu yan yana koydum:

/* 1) sıradan bir descendant selector editörün notuna da ulaşır */
.leaky a { color: inherit; text-decoration: none; }

/* 2) donut scope: karttan .user-content'e kadar, onun içine girmeden */
@scope (.scoped) to (.user-content) {
    a { color: inherit; text-decoration: none; }
}

/* 3) proximity: kaynak sırası ne olursa olsun daha yakın scope root kazanır */
@scope (.theme-dark) {
    a { color: rgb(147, 197, 253); }
}
@scope (.theme-light) {
    a { color: rgb(29, 78, 216); }
}

/* 4) scope root specificity eklemez: burada (0,1,0), aşağıdaki (0,2,0)'a karşı */
.scoped .price { color: rgb(185, 28, 28); }
@scope (.scoped) {
    .price { color: rgb(22, 163, 74); }
}
<article class="leaky">
    <h3><a href="book.html" id="leaky-title">Dune</a></h3>
    <div class="user-content"><p>Read the <a href="guide.html" id="leaky-note">reading guide</a> first.</p></div>
</article>
<article class="scoped">
    <h3><a href="book.html" id="scoped-title">Dune</a></h3>
    <p class="price" id="scoped-price">349.00 TRY</p>
    <div class="user-content"><p>Read the <a href="guide.html" id="scoped-note">reading guide</a> first.</p></div>
</article>
--- chromium
leaky-title    rgb(0, 0, 0)         underline no
leaky-note     rgb(0, 0, 0)         underline no
scoped-title   rgb(0, 0, 0)         underline no
scoped-note    rgb(0, 0, 238)       underline yes
light          rgb(29, 78, 216)     underline yes
dark-in-light  rgb(147, 197, 253)   underline yes
light-in-dark  rgb(29, 78, 216)     underline yes
scoped price   rgb(185, 28, 28)
shadow p: margin-top 16px, weight 700, color rgb(75, 85, 99) | page p: weight 400
computed color: rgb(185, 28, 28)
✓ (unlayered)        .scoped .price                       (0,2,0)  rgb(185, 28, 28)       12-scope.html
✗ (unlayered)        .price                               (0,1,0)  rgb(22, 163, 74)       12-scope.html
chromium catalog: "Dune" rgb(249, 250, 251) underline no, "reading guide" rgb(158, 158, 255) underline yes

İlk iki satır sorunu gösteriyor: .leaky a hem başlığın hem notun altını çizmiyor ve nottaki bağlantı artık metinden ayırt edilemiyor. @scope (.scoped) to (.user-content) ise bir donut tanımlıyor: kural .scoped'tan (scope root) başlayıp .user-content'e (scope limit) kadar geçerli, limit'in kendisi ve içindekiler hariç. Başlık düz metin gibi, nottaki bağlantı ise altı çizili ve tarayıcının bağlantı rengiyle (rgb(0, 0, 238)) duruyor. Bookstore'da aynı kuralı kartın dosyasına ekledim:

/* kartın içinde bağlantılar düz metin gibi görünür (başlık bir bağlantı), ama yalnız editörün notuna kadar:
   .user-content'teki bağlantılar CMS'ten gelir ve alt çizgilerini korur */
@scope (.book-card) to (.user-content) {
    a {
        color: inherit;
        text-decoration: none;
    }
}

Çıktının son satırı katalog sayfası: "Dune" bağlantısı kartın metin rengini alıyor ve altı çizili değil; "reading guide" altı çizili. Bağlantının rengi burada rgb(158, 158, 255), lab'deki gibi koyu mavi değil. Sebebi koyu banda eklediğim tek satır:

/* koyu bant: içindeki component'ler --surface'i style query ile okur */
.featured {
    --surface: dark;
    /* bandın içindeki light-dark() token'ları ve tarayıcının kendi renkleri (bağlantılar, kontroller) de koyu olur */
    color-scheme: dark;
    padding: var(--space);
    border-radius: var(--radius);
    background-color: #1f2937;
    color: #f9fafb;
}

Bant artık color-scheme: dark; bu, bir önceki yazıdaki light-dark() token'larını ve tarayıcının kendi çizdiği renkleri (bağlantılar, form kontrolleri) bandın içinde koyu temaya çeviriyor. Bu satır olmadan nottaki bağlantı koyu zemin üzerinde koyu mavi kalıyordu. Kullanıcı içeriği senin yazmadığın HTML olduğu için ona ne class ne stil ekleyebilirsin; doğru araç, bileşen stillerini ondan uzak tutmak ve ortamın rengini doğru vermek.

Proximity ve Specificity

@scope cascade'e yeni bir adım ekliyor: scope proximity. İki scope'lu kural aynı element'e uyuyor ve specificity'leri eşitse, scope root'u element'e daha yakın olan kazanır; kaynak sırası ancak bundan sonra devreye girer. Lab'deki tema örneği:

<div class="theme-light"><a href="#" id="light">light</a>
    <div class="theme-dark"><a href="#" id="dark-in-light">dark inside light</a></div>
</div>
<div class="theme-dark">
    <div class="theme-light"><a href="#" id="light-in-dark">light inside dark</a></div>
</div>

Çıktıda dark-in-light açık mavi, light-in-dark koyu mavi. Kaynakta .theme-light kuralı sonra geliyor; scope olmasaydı her iki iç içe durumda da kazanırdı ve açık temanın içine yerleştirilmiş koyu bir bölümdeki bağlantılar yanlış renkte görünürdü. Proximity, en içteki temanın kazanmasını sağlıyor; bu, iç içe tema ve bileşenlerde tam olarak istenen davranış.

Specificity tarafında önemli bir ayrıntı var: scope root, içerideki selector'lere specificity eklemez. @scope (.scoped) { .price { … } } içindeki .price'ın specificity'si (0,1,0); DevTools da bunu gösteriyor. Daha önce yazılmış .scoped .price (0,2,0) bu yüzden kazanıyor ve fiyat kırmızı kalıyor. Proximity, specificity'den sonra uygulanan bir adım; daha yüksek specificity'li sıradan bir kuralı yenemez. Bu bir dezavantaj gibi görünse de aslında nesting'deki şişmenin tersi: scope'lu kurallar düşük specificity ile yazılır ve gerektiğinde kolayca ezilir.

Angular, ASP.NET Core CSS Isolation ve Shadow DOM ile Karşılaştırma

Stilleri bir component'e bağlamanın tek yolu @scope değil. Serinin container query yazısında kurduğumuz Angular projesindeki kart bileşenine bir başlık bağlantısı ve CMS'ten gelen notu [innerHTML] ile gösteren bir alan ekledim; bileşenin stilinde yine a { color: inherit; text-decoration: none; } var:

cd angular
npx ng build
cd ..
node scripts/12-angular.mjs
--- chromium
Emma           underline no, attributes: _ngcontent-ng-c2542839406 href
Dune           underline no, attributes: _ngcontent-ng-c2542839406 href
reading guide  underline yes, attributes: href
a[_ngcontent-ng-c2542839406] { color: inherit; text-decoration: none; }

Angular'ın varsayılan emulated encapsulation'ı build sırasında bileşenin template'indeki her element'e _ngcontent-… attribute'u ekliyor ve bileşenin bütün selector'lerini bu attribute ile daraltıyor: a kuralı a[_ngcontent-ng-c2542839406] oldu. [innerHTML] ile çalışma anında eklenen element'ler bu attribute'u almıyor, bu yüzden nottaki bağlantı kuralın dışında kaldı ve altı çizili. Bu örnekte istediğimiz sonuç bu, ama aynı sebeple bileşenin stilleri, child component'lerin ya da innerHTML'in içine hiç ulaşamıyor. Ulaşması gerekirse Angular'ın kullanımdan kaldırılmış (deprecated) olarak işaretlediği ::ng-deep'e başvurmak gerekiyor.

ASP.NET Core'un Razor view'lar için CSS isolation'ı aynı fikri sunucu tarafında uyguluyor: Index.cshtml.css gibi bir dosyadaki selector'ler build sırasında b- ile başlayan bir attribute ile daraltılıyor ve view'ın element'leri bu attribute'u alıyor; child içerik için ::deep kullanılıyor. Ayrıntısı ASP.NET Core MVC (Model-View-Controller; veriyi, görünümü ve isteği karşılayan kodu ayıran yapı) serisinin Static Asset'ler ve UI Altyapısı yazısında. Shadow DOM ise ayrımı tarayıcının kendisine yaptırıyor. Lab'deki <book-note> custom element'i:

<!-- 5) Shadow DOM: sayfanın kuralları dışarıda kalır, inherit edilen property'ler yine de gelir -->
<section style="color: rgb(75, 85, 99)">
    <book-note id="note"></book-note>
</section>
<p id="light-dom-p">A paragraph outside the shadow root.</p>
<script>
    customElements.define('book-note', class extends HTMLElement {
        constructor() {
            super();
            this.attachShadow({ mode: 'open' }).innerHTML =
                '<style>p { font-weight: 700; }</style><p>Staff pick of the week.</p>';
        }
    });
</script>

Çıktıdaki "shadow p" satırı: sayfanın p { margin: 0; } kuralı shadow root'un içine girmedi (margin 16px, tarayıcının varsayılanı), shadow root'un içindeki p { font-weight: 700; } de dışarı çıkmadı (sayfadaki paragraf 400). Inherit edilen property'ler ise sınırı geçiyor: renk host'un ebeveyninden geldi. Custom property'ler de inherit edildiği için bir tasarım sisteminin token'ları shadow root'a ulaşabiliyor.

YöntemAyrımı kim yapıyorSayfanın stilleri içeri giriyor muNe zaman
Native nestingKimse; yalnız yazım kolaylığıEvetHer yerde, component'in kendi parçaları ve durumları için
@scopeTarayıcı, selector eşleşmesindeEvetBuild adımı olmadan component sınırı; kullanıcı içeriğini dışarıda bırakmak; iç içe temalar
Angular emulatedAngular build'i, attribute ileEvetAngular uygulamalarında varsayılan; bileşen stillerinin dışarı sızmaması
ASP.NET Core CSS isolation.NET build'i, attribute ileEvetRazor view ve component'leri; MVC ve Razor Pages
Shadow DOMTarayıcı, ayrı element ağacıylaHayır (yalnız inherit edilenler ve custom property'ler)Başka sayfalara gömülen widget'lar, design system web component'leri

Üretimde Dikkat

Native nesting Aralık 2023'te Chrome 120, Firefox 117 ve Safari 17.2 ile gevşetilmiş syntax'ıyla birlikte Baseline oldu ve Haziran 2026'dan beri widely available. @scope ise Firefox 146 ve Safari 26.4 ile Mart 2026'da Baseline oldu; newly available. Desteklemeyen bir tarayıcı @scope bloğunu bütünüyle yok sayar: Bookstore'da bu, kart başlığındaki bağlantının altının çizili görünmesi demek. Sayfa bozulmuyor, yalnız görünüm biraz farklı. Kullanıcı içeriğini dışarıda bırakmak gibi kritik bir iş için kullanıyorsan, eski tarayıcılar için güvenli tarafta kalan bir varsayılan seç.

Nesting'li bir dosyada declaration'ları her zaman nested kurallardan önce yaz. Bir nested kuraldan sonra gelen declaration'ların davranışı 2024'te spesifikasyonda değişti; eski ve yeni tarayıcıların farklı sonuç vermesine sebep olmamak için declaration'lar en üstte, iç içe kurallar altta durmalı. Bookstore'un kart dosyası bu düzende.

Preprocessor'ı bırakmak için tek sebep nesting değil. Sass'ın mixin'leri, döngüleri, module sistemi ve fonksiyonları hâlâ CSS'te yok ya da yeni yeni geliyor (@function henüz Baseline değil). Sass ile native CSS'i birlikte kullanmanın ayrıntısı Sass/SCSS serisinde.

Sık Düşülen Tuzaklar

Birincisi, Sass alışkanlığıyla &__title, &--active yazmak. Native CSS'te bunlar hiçbir şey seçmez ve hata da vermez.

İkincisi, ebeveyn listesinde bir id ya da yüksek specificity'li bir selector varken nesting yapmak. :is() en yüksek specificity'yi bütün listeye yayar.

Üçüncüsü, HTML'in yapısını CSS'e iç içe kopyalamak. Her seviye specificity ekler; iki seviyeyi geçen nesting genellikle bir uyarı işaretidir.

Dördüncüsü, @scope'u specificity aracı sanmak. Scope root specificity eklemez; proximity yalnız eşit specificity'li kurallar arasında karar verir.

Küçük Bir Deneme

★☆☆ Lab'deki #featured, .book-card { & .price { … } } kuralını iki ayrı kurala böl ve node scripts/12-nesting.mjs çıktısında fiyatın specificity'sinin nasıl değiştiğini kontrol et.

★★☆ form.css'i nesting ile yeniden yaz ve 12-fingerprint.mjs'i checkout.html'in form satırlarını da kapsayacak şekilde genişlet. Dosyayı değiştirmeden önce ve sonra hash'lerin aynı kaldığını göster.

★★★ book.html'deki özet paragrafını CMS'ten gelen bir .user-content bloğuna çevir (içinde bir bağlantı ve bir liste olsun) ve kitap detay sayfasının bileşen stillerini @scope (.book-page) to (.user-content) içine al. Kart ve sayfa bileşeni kurallarının kullanıcı içeriğine ulaşmadığını, inherit edilen renk ve font'un ise geldiğini bir Playwright script'iyle doğrula.

Bir sonraki yazıda bu serinin hareketli kısmına geçiyoruz: transition ve animation farkı, easing ve linear(), @starting-style ile display: none'dan açılan bir sepet dialog'u ve prefers-reduced-motion ile animasyonu kapatmak yerine azaltmak.