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

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

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

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

Поддержка браузера

  • Хром: 130.
  • Край: 130.
  • Фаерфокс: 132.
  • Сафари: не поддерживается.

Проблема с вложенностью 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.

Поддержка браузера

  • Хром: 130.
  • Край: 130.
  • Фаерфокс: 132.
  • Сафари: не поддерживается.

Введение правила вложенных объявлений изменяет синтаксический анализатор 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
}