CSS iç içe yerleştirme, CSSNestedDeclarations ile iyileştirildi

Yayınlanma tarihi: 8 Ekim 2024

CSS iç içe yerleştirmeyle ilgili bazı garip tuhaflıkları düzeltmek için CSS Çalışma Grubu, CSSNestedDeclarations arayüzünü CSS İç İçe Yerleştirme Spesifikasyonu'na eklemeye karar verdi. Bu eklemeyle birlikte, bazı iyileştirmelerin yanı sıra stil kurallarından sonra gelen bildirimler artık yukarı kaymıyor.

Bu değişiklikler, Chrome'un 130 sürümünden itibaren kullanılabilir ve Firefox Nightly 132 ile Safari Technology Preview 204'te test edilmeye hazırdır.

Tarayıcı desteği

  • Chrome: 130.
  • Kenar: 130.
  • Firefox: 132.
  • Safari: Desteklenmez.

CSSNestedDeclarations olmadan CSS iç içe yerleştirme sorunu

CSS iç içe yerleştirmeyle ilgili dikkat edilmesi gereken noktalardan biri, aşağıdaki snippet'in başlangıçta beklediğiniz gibi çalışmamasıdır:

.foo {
    width: fit-content;

    @media screen {
        background-color: red;
    }
    
    background-color: green;
}

Koda baktığınızda, background-color: green; beyanı en son geldiği için <div class=foo> öğesinin bir green background-color içerdiğini varsayabilirsiniz. Ancak bu durum, 130'dan önceki Chrome'da geçerli değildir. CSSNestedDeclarations desteğinin bulunmadığı bu sürümlerde öğenin background-color değeri red olur.

130 kullanımdan önceki Chrome'un gerçek kuralı ayrıştırıldıktan sonra aşağıdaki gibidir:

.foo {
    width: fit-content;
    background-color: green;

    @media screen {
        & {
            background-color: red;
        }
    }
}

Ayrıştırma işleminden sonra CSS'de iki değişiklik yapıldı:

  • background-color: green;, diğer iki beyanla birleştirmek için kaydırıldı.
  • İç içe yerleştirilmiş CSSMediaRule, & seçici kullanılarak beyanlarını ek bir CSSStyleRule içine sarmalamak için yeniden yazıldı.

Burada göreceğiniz diğer bir tipik değişiklik de ayrıştırıcının desteklemediği özellikleri atmasıdır.

CSSStyleRule sayfasındaki cssText bölümünü okuyarak "Ayrıştırdıktan sonra CSS"yi kendiniz inceleyebilirsiniz.

Bu etkileşimli oyun alanında kendiniz deneyin:

Bu CSS neden yeniden yazılıyor?

Bu dahili yeniden yazma işleminin neden gerçekleştiğini anlamak için bu CSSStyleRule değerinin CSS Nesne Modeli'nde (CSSOM) nasıl temsil edildiğini anlamanız gerekir.

130'dan önceki Chrome'da, daha önce paylaşılan CSS snippet'i aşağıdaki şekilde serileştirilir:

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .resolvedSelectorText = ".foo"
  .specificity = "(0,1,0)"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ CSSMediaRule
    .type = MEDIA_RULE
    .cssRules (CSSRuleList, 1) =
      ↳ CSSStyleRule
        .type = STYLE_RULE
        .selectorText = "&"
        .resolvedSelectorText = ":is(.foo)"
        .specificity = "(0,1,0)"
        .style (CSSStyleDeclaration, 1) =
          - background-color: red

Bir CSSStyleRule öğesinin sahip olduğu tüm özelliklerden aşağıdaki ikisi bu durumda alakalıdır:

  • Tanımlamaları temsil eden bir CSSStyleDeclaration örneği olan style mülkü.
  • Tüm iç içe yerleştirilmiş CSSRule nesnelerini içeren bir CSSRuleList olan cssRules özelliği.

CSS snippet'indeki tüm beyanlar CSStyleRule öğesinin style mülkünde sona erdiği için bilgi kaybı yaşanır. style özelliğine baktığımızda, background-color: green öğesinin iç içe yerleştirilmiş CSSMediaRule öğesinden sonra tanımlandığı anlaşılmamaktadır.

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ …

CSS motorunun düzgün çalışması için stil kuralının içeriğinin başında görünen özellikleri diğer kurallarla birlikte görünen özelliklerden ayırt edebilmesi gerekir. Bu nedenle, bu durum sorunlu bir durumdur.

CSSMediaRule içindeki beyanlar aniden CSSStyleRule içine yerleştiriliyor. Bunun nedeni, CSSMediaRule öğesinin beyan içerecek şekilde tasarlanmamış olmasıdır.

CSSMediaRule, cssRules mülkü aracılığıyla erişilebilen iç içe yerleştirilmiş kurallar içerebileceğinden, bildirimler otomatik olarak bir CSSStyleRule içine alınır.

↳ CSSMediaRule
  .type = MEDIA_RULE
  .cssRules (CSSRuleList, 1) =
    ↳ CSSStyleRule
      .type = STYLE_RULE
      .selectorText = "&"
      .resolvedSelectorText = ":is(.foo)"
      .specificity = "(0,1,0)"
      .style (CSSStyleDeclaration, 1) =
        - background-color: red

Bu sorunu nasıl çözebilirim?

CSS Çalışma Grubu, bu sorunu çözmek için çeşitli seçenekleri inceledi.

Önerilen çözümlerden biri, tüm salt bildirimleri iç içe yerleştirme seçicisiyle (&) iç içe yerleştirilmiş bir CSSStyleRule içine sarmalamaktı. Bu fikir, &'un :is(…) olarak ayrıştırılmasının aşağıdaki istenmeyen yan etkileri de dahil olmak üzere çeşitli nedenlerle reddedildi:

  • Belirlilik üzerinde bir etkisi vardır. Bunun nedeni, :is()'ün en spesifik bağımsız değişkeninin özgünlüğünü devralmasıdır.
  • Orijinal dış seçicideki sözde öğelerle iyi çalışmaz. Bunun nedeni, :is()'ün seçici listesi bağımsız değişkeninde sözde öğeleri kabul etmemesidir.

Aşağıdaki örneği inceleyelim:

#foo, .foo, .foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    background-color: green;
  }
}

Bu snippet ayrıştırıldıktan sonra, Chrome'da 130'dan önce şu hale gelir:

#foo,
.foo,
.foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    & {
      background-color: green;
    }
  }
}

& seçicisiyle iç içe yerleştirilmiş CSSRule nedeniyle bu bir sorundur:

  • :is(#foo, .foo) değerine kadar düzleştirilir ve bu sırada seçici listesinden .foo::before atılır.
  • (1,0,0) özgünlüğü vardır ve bu, daha sonra üzerine yazılmasını zorlaştırır.

Kuralın neyi serileştirdiğini inceleyerek bunu kontrol edebilirsiniz:

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = "#foo, .foo, .foo::before"
  .resolvedSelectorText = "#foo, .foo, .foo::before"
  .specificity = (1,0,0),(0,1,0),(0,1,1)
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: red
  .cssRules (CSSRuleList, 1) =
    ↳ CSSMediaRule
      .type = MEDIA_RULE
      .cssRules (CSSRuleList, 1) =
        ↳ CSSStyleRule
          .type = STYLE_RULE
          .selectorText = "&"
          .resolvedSelectorText = ":is(#foo, .foo, .foo::before)"
          .specificity = (1,0,0)
          .style (CSSStyleDeclaration, 1) =
            - background-color: green

Görsel olarak da .foo::before için background-color değerinin green yerine red olduğu anlamına gelir.

CSS Çalışma Grubu'nun incelediği bir diğer yaklaşım, tüm iç içe yerleştirilmiş bildirimleri bir @nest kuralına sarmalamaktı. Bu öneri, geliştirici deneyiminde gerileme yaratacağı için reddedildi.

CSSNestedDeclarations arayüzü kullanıma sunuldu

CSS Çalışma Grubu, iç içe yerleştirilmiş bildirim kuralını uygulamaya karar verdi.

Bu iç içe yerleştirilmiş beyan kuralı, Chrome 130'dan itibaren Chrome'da uygulanmaktadır.

Tarayıcı desteği

  • Chrome: 130.
  • Kenar: 130.
  • Firefox: 132.
  • Safari: Desteklenmez.

İç içe yerleştirilmiş beyan kuralı, CSS ayrıştırıcısını doğrudan iç içe yerleştirilmiş art arda beyanları otomatik olarak bir CSSNestedDeclarations örneğine sarmalayacak şekilde değiştirir. Bu CSSNestedDeclarations örneği, serileştirildiğinde CSSStyleRule öğesinin cssRules mülkünde yer alır.

Örnek olarak aşağıdaki CSSStyleRule tekrar ele alınır:

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

Chrome 130 veya sonraki sürümlerde serileştirildiğinde aşağıdaki gibi görünür:

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .resolvedSelectorText = ".foo"
  .specificity = (0,1,0)
  .style (CSSStyleDeclaration, 1) =
    - width: fit-content
  .cssRules (CSSRuleList, 2) =
    ↳ CSSMediaRule
      .type = MEDIA_RULE
      .cssRules (CSSRuleList, 1) =
        ↳ CSSNestedDeclarations
          .style (CSSStyleDeclaration, 1) =
            - background-color: red
    ↳ CSSNestedDeclarations
      .style (CSSStyleDeclaration, 1) =
        - background-color: green

CSSNestedDeclarations kuralı CSSRuleList içinde sona erdiğinden ayrıştırıcı, background-color: green beyanının konumunu koruyabilir: background-color: red beyanının (CSSMediaRule'nin bir parçasıdır) sonrasında.

Ayrıca, bir CSSNestedDeclarations örneğinin kullanılması, artık reddedilmiş olan diğer olası çözümlerin neden olduğu kötü yan etkilerden hiçbirini ortaya çıkarmaz: İç içe yerleştirilmiş beyanlar kuralı, aynı özgünlük davranışıyla üst stil kuralıyla tam olarak aynı öğeleri ve sözde öğeleri eşleştirir.

Bunun kanıtı, CSSStyleRule'un cssText değerini geri okumaktır. İç içe yerleştirilmiş bildirimler kuralı sayesinde, giriş CSS'siyle aynıdır:

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

Bunun sizin için anlamı:

Bu, CSS iç içe yerleştirme özelliğinin Chrome 130'dan itibaren çok daha iyi hale geldiği anlamına gelir. Ancak, salt beyanları iç içe yerleştirilmiş kurallarla iç içe yerleştiriyorsanız kodunuzun bir kısmını gözden geçirmeniz gerekebilir.

@starting-style özelliğinin kullanıldığı aşağıdaki örneği ele alalım

/* This does not work in Chrome 130 */
#mypopover:popover-open {
  @starting-style {
    opacity: 0;
    scale: 0.5;
  }

  opacity: 1;
  scale: 1;
}

Chrome 130'tan önce bu bildirimler kaldırılırdı. opacity: 1; ve scale: 1; beyanları CSSStyleRule.style'ye, ardından CSSStyleRule.cssRules'te bir CSSStartingStyleRule (@starting-style kuralını temsil eder) eklenir.

Chrome 130'dan itibaren, bildirimler artık kaldırılmaz ve CSSStyleRule.cssRules içinde iki iç içe yerleştirilmiş CSSRule nesnesi elde edersiniz. Sırasıyla: bir CSSStartingStyleRule (@starting-style kuralını temsil eder) ve opacity: 1; scale: 1; beyanlarını içeren bir CSSNestedDeclarations.

Bu davranış değişikliği nedeniyle, @starting-style beyanlarının üzerine CSSNestedDeclarations örneğinde bulunanlar yazılır ve giriş animasyonu kaldırılır.

Kodu düzeltmek için @starting-style bloğunun normal bildirimlerin ardından geldiğinden emin olun. Örneğin:

/* This works in Chrome 130 */
#mypopover:popover-open {
  opacity: 1;
  scale: 1;

  @starting-style {
    opacity: 0;
    scale: 0.5;
  }
}

CSS iç içe yerleştirme özelliğini kullanırken iç içe yerleştirilmiş beyanlarınızı iç içe yerleştirilmiş kuralların üzerinde tutarsanız kodunuz, CSS iç içe yerleştirmeyi destekleyen tüm tarayıcıların çoğunda sorunsuz çalışır.

Son olarak, CSSNestedDeclarations özelliğinin kullanılabilir olup olmadığını tespit etmek istiyorsanız aşağıdaki JavaScript snippet'ini kullanabilirsiniz:

if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
  // CSSNestedDeclarations is not available
}