Вложение CSS улучшается с помощью CSSNestedDeclarations, вложение CSS улучшается с помощью CSSNestedDeclarations

Опубликовано: 8 октября 2024 г.

Чтобы исправить некоторые странные особенности вложенности CSS, рабочая группа CSS решила добавить интерфейс CSSNestedDeclarations в Спецификацию вложенности CSS . Благодаря этому дополнению объявления, следующие после правил стиля, больше не смещаются вверх, а также некоторые другие улучшения.

Эти изменения доступны в Chrome начиная с версии 130 и готовы к тестированию в Firefox Nightly 132 и Safari Technology Preview 204.

Проблема с вложенностью CSS без CSSNestedDeclarations

Одна из проблем с вложенностью CSS заключается в том, что изначально следующий фрагмент не работает так, как вы могли ожидать:

.foo {
   
width: fit-content;

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

Глядя на код, можно предположить, что элемент <div class=foo> имеет green background-color потому что background-color: green; декларация идет последней. Но в Chrome до версии 130 это не так. В тех версиях, где отсутствует поддержка CSSNestedDeclarations , background-color элемента — red .

После анализа фактического правила Chrome до 130 использований выглядит следующее:

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

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

CSS после парсинга претерпел два изменения:

  • background-color: green; был перенесен вверх, чтобы присоединиться к двум другим декларациям.
  • Вложенный CSSMediaRule был переписан, чтобы его объявления были заключены в дополнительный CSSStyleRule с использованием селектора & .

Еще одно типичное изменение, которое вы здесь увидите, — это отбрасывание парсером свойств, которые он не поддерживает.

Вы можете проверить «CSS после синтаксического анализа» самостоятельно, прочитав cssText из CSSStyleRule .

Попробуйте сами на этой интерактивной игровой площадке :

Почему этот CSS переписан?

Чтобы понять, почему произошла эта внутренняя перезапись, вам необходимо понять, как это CSSStyleRule представлено в объектной модели CSS (CSSOM).

В Chrome до версии 130 фрагмент CSS, опубликованный ранее, сериализуется в следующее:

↳ 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

Из всех свойств, которыми обладает CSSStyleRule , в данном случае актуальны следующие два:

  • Свойство style , которое представляет собой экземпляр CSSStyleDeclaration представляющий объявления.
  • Свойство cssRules , которое представляет собой CSSRuleList , содержащее все вложенные объекты CSSRule .

Поскольку все объявления из фрагмента CSS попадают в свойство style CSStyleRule , происходит потеря информации. При взгляде на свойство style неясно, что background-color: green был объявлен после вложенного CSSMediaRule .

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

Это проблематично, поскольку для правильной работы механизма CSS он должен уметь отличать свойства, которые появляются в начале содержимого правила стиля, от тех, которые появляются вперемежку с другими правилами.

Что касается объявлений внутри CSSMediaRule которые внезапно оказались заключены в CSSStyleRule : это потому, что CSSMediaRule не был разработан для содержания объявлений.

Поскольку CSSMediaRule может содержать вложенные правила, доступные через свойство cssRules , объявления автоматически заключаются в CSSStyleRule .

↳ 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

Как это решить?

Рабочая группа CSS рассмотрела несколько вариантов решения этой проблемы.

Одним из предложенных решений было обернуть все пустые объявления во вложенный CSSStyleRule с помощью селектора вложенности ( & ). Эта идея была отвергнута по разным причинам, включая следующие нежелательные побочные эффекты & обессахаривания :is(…) :

  • Это влияет на специфику. Это связано с тем, что :is() перенимает специфику своего наиболее конкретного аргумента.
  • Это не очень хорошо работает с псевдоэлементами в исходном внешнем селекторе. Это связано с тем, что :is() не принимает псевдоэлементы в своем аргументе списка селекторов.

Возьмем следующий пример:

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

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

После анализа этого фрагмента в Chrome до 130 он становится таким:

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

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

Это проблема, поскольку вложенный CSSRule с селектором & :

  • Сглаживается до :is(#foo, .foo) , попутно удаляя .foo::before из списка селектора.
  • Имеет специфичность (1,0,0) , что затрудняет последующую перезапись.

Вы можете проверить это, проверив, во что сериализуется правило:

↳ 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

Визуально это также означает, что background-color .foo::before red а не green .

Другой подход, который рассматривала рабочая группа CSS, заключался в том, чтобы заключить все вложенные объявления в правило @nest . Это было отклонено из-за ухудшения опыта разработчиков, которое это могло бы вызвать.

Знакомство с интерфейсом CSSNestedDeclarations

Решением, на котором остановилась рабочая группа CSS, является введение правила вложенных объявлений .

Это правило вложенных объявлений реализовано в Chrome, начиная с Chrome 130.

Введение правила вложенных объявлений изменяет синтаксический анализатор CSS, который автоматически переносит последовательные непосредственно вложенные объявления в экземпляр CSSNestedDeclarations . При сериализации этот экземпляр CSSNestedDeclarations попадает в свойство cssRules CSSStyleRule .

Снова возьмем в качестве примера следующий CSSStyleRule :

.foo {
 
width: fit-content;

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

При сериализации в Chrome 130 или новее это выглядит так:

↳ 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 заканчивается в CSSRuleList , синтаксический анализатор может сохранить позицию объявления background-color: green : после объявления background-color: red (которое является частью CSSMediaRule ).

Более того, наличие экземпляра CSSNestedDeclarations не приводит к каким-либо неприятным побочным эффектам, вызванным другими, теперь отброшенными потенциальными решениями: правило вложенных объявлений соответствует тем же самым элементам и псевдоэлементам, что и его родительское правило стиля, с той же специфичностью. поведение.

Доказательством этого является чтение cssText CSSStyleRule . Благодаря правилу вложенных объявлений он такой же, как и входной CSS:

.foo {
 
width: fit-content;

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

Что это значит для вас

Это означает, что вложенность CSS стала намного лучше в Chrome 130. Но это также означает, что вам, возможно, придется просмотреть часть вашего кода, если вы чередуете голые объявления с вложенными правилами.

Возьмем следующий пример, в котором используется замечательный @starting-style

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

 
opacity: 1;
 
scale: 1;
}

До Chrome 130 эти декларации были подняты. В итоге вы получите opacity: 1; и scale: 1; объявления, входящие в CSSStyleRule.style , за которыми следует CSSStartingStyleRule (представляющий правило @starting-style ) в CSSStyleRule.cssRules .

Начиная с Chrome 130, объявления больше не поднимаются, и в итоге вы получаете два вложенных объекта CSSRule в CSSStyleRule.cssRules . По порядку: один CSSStartingStyleRule (представляющий правило @starting-style ) и один CSSNestedDeclarations , содержащий opacity: 1; scale: 1; декларации.

Из-за этого измененного поведения объявления @starting-style перезаписываются теми, которые содержатся в экземпляре CSSNestedDeclarations , тем самым удаляя анимацию ввода.

Чтобы исправить код, убедитесь, что блок @starting-style идет после обычных объявлений. Вот так:

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

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

Если при использовании вложенности CSS вы держите вложенные объявления поверх вложенных правил, ваш код в основном работает нормально со всеми версиями всех браузеров, поддерживающих вложенность CSS.

Наконец, если вы хотите определить доступность CSSNestedDeclarations , вы можете использовать следующий фрагмент JavaScript:

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

Опубликовано: 8 октября 2024 г.

Чтобы исправить некоторые странные особенности вложенности CSS, рабочая группа CSS решила добавить интерфейс CSSNestedDeclarations в Спецификацию вложенности CSS . Благодаря этому дополнению объявления, следующие после правил стиля, больше не смещаются вверх, а также некоторые другие улучшения.

Эти изменения доступны в Chrome начиная с версии 130 и готовы к тестированию в Firefox Nightly 132 и Safari Technology Preview 204.

Проблема с вложенностью CSS без CSSNestedDeclarations

Одна из проблем с вложенностью CSS заключается в том, что изначально следующий фрагмент не работает так, как вы могли ожидать:

.foo {
   
width: fit-content;

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

Глядя на код, можно предположить, что элемент <div class=foo> имеет green background-color потому что background-color: green; декларация идет последней. Но в Chrome до версии 130 это не так. В тех версиях, где отсутствует поддержка CSSNestedDeclarations , background-color элемента — red .

После анализа фактического правила Chrome до 130 использований выглядит следующее:

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

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

CSS после парсинга претерпел два изменения:

  • background-color: green; был перенесен вверх, чтобы присоединиться к двум другим декларациям.
  • Вложенный CSSMediaRule был переписан, чтобы его объявления были заключены в дополнительный CSSStyleRule с использованием селектора & .

Еще одно типичное изменение, которое вы здесь увидите, — это отбрасывание парсером свойств, которые он не поддерживает.

Вы можете проверить «CSS после анализа» самостоятельно, прочитав cssText из CSSStyleRule .

Попробуйте сами на этой интерактивной игровой площадке :

Почему этот CSS переписан?

Чтобы понять, почему произошла эта внутренняя перезапись, вам необходимо понять, как это CSSStyleRule представлено в объектной модели CSS (CSSOM).

В Chrome до версии 130 фрагмент CSS, опубликованный ранее, сериализуется в следующее:

↳ 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

Из всех свойств, которыми обладает CSSStyleRule , в данном случае актуальны следующие два:

  • Свойство style , которое представляет собой экземпляр CSSStyleDeclaration представляющий объявления.
  • Свойство cssRules , которое представляет собой CSSRuleList , содержащее все вложенные объекты CSSRule .

Поскольку все объявления из фрагмента CSS попадают в свойство style CSStyleRule , происходит потеря информации. При взгляде на свойство style неясно, что background-color: green был объявлен после вложенного CSSMediaRule .

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

Это проблематично, поскольку для правильной работы механизма CSS он должен уметь отличать свойства, которые появляются в начале содержимого правила стиля, от тех, которые появляются вперемежку с другими правилами.

Что касается объявлений внутри CSSMediaRule которые внезапно оказались заключены в CSSStyleRule : это потому, что CSSMediaRule не был разработан для содержания объявлений.

Поскольку CSSMediaRule может содержать вложенные правила, доступные через свойство cssRules , объявления автоматически заключаются в CSSStyleRule .

↳ 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

Как это решить?

Рабочая группа CSS рассмотрела несколько вариантов решения этой проблемы.

Одним из предложенных решений было обернуть все пустые объявления во вложенный CSSStyleRule с помощью селектора вложенности ( & ). Эта идея была отвергнута по разным причинам, включая следующие нежелательные побочные эффекты & обессахаривания :is(…) :

  • Это влияет на специфику. Это связано с тем, что :is() перенимает специфику своего наиболее конкретного аргумента.
  • Это не очень хорошо работает с псевдоэлементами в исходном внешнем селекторе. Это связано с тем, что :is() не принимает псевдоэлементы в своем аргументе списка селекторов.

Возьмем следующий пример:

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

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

После анализа этого фрагмента в Chrome до 130 он становится таким:

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

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

Это проблема, поскольку вложенный CSSRule с селектором & :

  • Сглаживается до :is(#foo, .foo) , попутно удаляя .foo::before из списка селектора.
  • Имеет специфичность (1,0,0) , что затрудняет последующую перезапись.

Вы можете проверить это, проверив, во что сериализуется правило:

↳ 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

Визуально это также означает, что background-color .foo::before red а не green .

Другой подход, который рассматривала рабочая группа CSS, заключался в том, чтобы заключить все вложенные объявления в правило @nest . Это было отклонено из-за ухудшения опыта разработчиков, которое это могло бы вызвать.

Знакомство с интерфейсом CSSNestedDeclarations

Решением, на котором остановилась рабочая группа CSS, является введение правила вложенных объявлений .

Это правило вложенных объявлений реализовано в Chrome, начиная с Chrome 130.

Введение правила вложенных объявлений изменяет синтаксический анализатор CSS, который автоматически переносит последовательные непосредственно вложенные объявления в экземпляр CSSNestedDeclarations . При сериализации этот экземпляр CSSNestedDeclarations попадает в свойство cssRules CSSStyleRule .

Снова возьмем в качестве примера следующий CSSStyleRule :

.foo {
 
width: fit-content;

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

При сериализации в Chrome 130 или новее это выглядит так:

↳ 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 заканчивается в CSSRuleList , синтаксический анализатор может сохранить позицию объявления background-color: green : после объявления background-color: red (которое является частью CSSMediaRule ).

Более того, наличие экземпляра CSSNestedDeclarations не приводит к каким-либо неприятным побочным эффектам, вызванным другими, теперь отброшенными потенциальными решениями: правило вложенных объявлений соответствует тем же самым элементам и псевдоэлементам, что и его родительское правило стиля, с той же специфичностью. поведение.

Доказательством этого является чтение cssText CSSStyleRule . Благодаря правилу вложенных объявлений он такой же, как и входной CSS:

.foo {
 
width: fit-content;

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

Что это значит для вас

Это означает, что вложенность CSS стала намного лучше в Chrome 130. Но это также означает, что вам, возможно, придется просмотреть часть вашего кода, если вы чередуете голые объявления с вложенными правилами.

Возьмем следующий пример, в котором используется замечательный @starting-style

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

 
opacity: 1;
 
scale: 1;
}

До Chrome 130 эти декларации были подняты. В итоге вы получите opacity: 1; и scale: 1; объявления, входящие в CSSStyleRule.style , за которыми следует CSSStartingStyleRule (представляющий правило @starting-style ) в CSSStyleRule.cssRules .

Начиная с Chrome 130, объявления больше не поднимаются, и в итоге вы получаете два вложенных объекта CSSRule в CSSStyleRule.cssRules . По порядку: один CSSStartingStyleRule (представляющий правило @starting-style ) и один CSSNestedDeclarations , содержащий opacity: 1; scale: 1; декларации.

Из-за этого измененного поведения объявления @starting-style перезаписываются теми, которые содержатся в экземпляре CSSNestedDeclarations , тем самым удаляя анимацию ввода.

Чтобы исправить код, убедитесь, что блок @starting-style идет после обычных объявлений. Вот так:

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

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

Если при использовании вложенности CSS вы держите вложенные объявления поверх вложенных правил, ваш код в основном работает нормально со всеми версиями всех браузеров, поддерживающих вложенность CSS.

Наконец, если вы хотите определить доступность CSSNestedDeclarations , вы можете использовать следующий фрагмент JavaScript:

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