Publicado em 8 de outubro de 2024
Para corrigir algumas peculiaridades estranhas com o aninhamento do CSS, o grupo de trabalho do CSS resolveu adicionar a interface CSSNestedDeclarations
à especificação de aninhamento do CSS. Com essa adição, as declarações que vêm após as regras de estilo não são mais deslocadas para cima, entre outras melhorias.
Essas mudanças estão disponíveis no Chrome a partir da versão 130 e estão prontas para testes no Firefox Nightly 132 e Safari Technology Preview 204.
Compatibilidade com navegadores
O problema com o aninhamento de CSS sem CSSNestedDeclarations
Um dos problemas com o aninhamento de CSS é que, originalmente, o snippet a seguir não funciona como você espera:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Analisando o código, você pode supor que o elemento <div class=foo>
tem um background-color
green
porque a declaração background-color: green;
vem por último. Mas esse não é o caso do Chrome antes da versão 130. Nessas versões, que não oferecem suporte a CSSNestedDeclarations
, o background-color
do elemento é red
.
Após analisar a regra em si, o Chrome antes da versão 130 fica assim:
.foo {
width: fit-content;
background-color: green;
@media screen {
& {
background-color: red;
}
}
}
O CSS após a análise passou por duas mudanças:
- O
background-color: green;
foi deslocado para cima para unir as outras duas declarações. - O
CSSMediaRule
aninhado foi reescrito para unir as declarações em umCSSStyleRule
extra usando o seletor&
.
Outra mudança típica que você vai encontrar aqui é o parser descartando propriedades que não são compatíveis.
Para inspecionar o "CSS após a análise", leia novamente o cssText
do CSSStyleRule
.
Teste você mesmo neste playground interativo:
Por que esse CSS foi reescrito?
Para entender por que essa substituição interna aconteceu, você precisa entender como esse CSSStyleRule
é representado no CSS Object Model (CSSOM).
No Chrome anterior à versão 130, o snippet de CSS compartilhado anteriormente é serializado para o seguinte:
↳ 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
De todas as propriedades de um CSSStyleRule
, as duas seguintes são relevantes neste caso:
- A propriedade
style
, que é uma instânciaCSSStyleDeclaration
que representa as declarações. - A propriedade
cssRules
, que é umCSSRuleList
que contém todos os objetosCSSRule
aninhados.
Como todas as declarações do snippet de CSS acabam na propriedade style
do CSStyleRule
, há uma perda de informações. Ao analisar a propriedade style
, não está claro se o background-color: green
foi declarado após o CSSMediaRule
aninhado.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ …
Isso é problemático porque, para que um mecanismo CSS funcione corretamente, ele precisa distinguir as propriedades que aparecem no início do conteúdo de uma regra de estilo das que aparecem intercaladas com outras regras.
Quanto às declarações dentro do CSSMediaRule
que são repentinamente agrupadas em um CSSStyleRule
, isso ocorre porque o CSSMediaRule
não foi projetado para conter declarações.
Como CSSMediaRule
pode conter regras aninhadas, que podem ser acessadas pela propriedade cssRules
, as declarações são automaticamente agrupadas em um 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
Como resolver isso?
O grupo de trabalho dos CSS analisou várias opções para resolver esse problema.
Uma das soluções sugeridas foi agrupar todas as declarações simples em um CSSStyleRule
aninhado com o seletor de aninhamento (&
). Essa ideia foi descartada por vários motivos, incluindo os seguintes efeitos colaterais indesejados da simplificação de &
para :is(…)
:
- Isso afeta a especificidade. Isso ocorre porque
:is()
assume a especificidade do argumento mais específico. - Ele não funciona bem com pseudoelementos no seletor externo original. Isso ocorre porque
:is()
não aceita pseudoelementos no argumento da lista de seletores.
Veja este exemplo:
#foo, .foo, .foo::before {
width: fit-content;
background-color: red;
@media screen {
background-color: green;
}
}
Depois de analisar esse snippet, ele se torna o seguinte no Chrome antes da versão 130:
#foo,
.foo,
.foo::before {
width: fit-content;
background-color: red;
@media screen {
& {
background-color: green;
}
}
}
Isso é um problema porque o CSSRule
aninhado com o seletor &
:
- Achata para
:is(#foo, .foo)
, descartando.foo::before
da lista de seletores. - Tem uma especificidade de
(1,0,0)
que dificulta a substituição posterior.
Para verificar isso, inspecione o que a regra serializa:
↳ 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
Visualmente, isso também significa que o background-color
de .foo::before
é red
em vez de green
.
Outra abordagem analisada pelo Grupo de Trabalho do CSS foi a de agrupar todas as declarações aninhadas em uma regra @nest
. Isso foi descartado devido à experiência regressiva que isso causaria para os desenvolvedores.
Introdução à interface CSSNestedDeclarations
A solução do grupo de trabalho do CSS foi a introdução da regra de declarações aninhadas.
Essa regra de declarações aninhadas é implementada no Chrome a partir da versão 130.
Compatibilidade com navegadores
A introdução da regra de declarações aninhadas muda o analisador de CSS para agrupar automaticamente declarações aninhadas diretamente consecutivas em uma instância CSSNestedDeclarations
. Quando serializada, essa instância de CSSNestedDeclarations
acaba na propriedade cssRules
de CSSStyleRule
.
Usando o CSSStyleRule
a seguir como exemplo:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Quando serializado no Chrome 130 ou mais recente, fica assim:
↳ 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
Como a regra CSSNestedDeclarations
termina no CSSRuleList
, o analisador consegue manter a posição da declaração background-color: green
: após a declaração background-color: red
, que faz parte do CSSMediaRule
.
Além disso, ter uma instância CSSNestedDeclarations
não introduz nenhum dos efeitos colaterais desagradáveis que as outras soluções, agora descartadas, causaram: a regra de declarações aninhadas corresponde aos mesmos elementos e pseudoelementos da regra de estilo pai, com o mesmo comportamento de especificidade.
A prova disso é ler novamente o cssText
do CSSStyleRule
. Graças à regra de declarações aninhadas, ele é igual ao CSS de entrada:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
O que isso significa para você
Isso significa que a aninhamento de CSS ficou muito melhor a partir do Chrome 130. No entanto, isso também significa que talvez seja necessário revisar parte do código se você estiver intercalando declarações simples com regras aninhadas.
Confira o exemplo a seguir que usa o maravilhoso @starting-style
.
/* This does not work in Chrome 130 */
#mypopover:popover-open {
@starting-style {
opacity: 0;
scale: 0.5;
}
opacity: 1;
scale: 1;
}
Antes do Chrome 130, essas declarações eram suspensas. As declarações opacity: 1;
e scale: 1;
vão para o CSSStyleRule.style
, seguidas por uma CSSStartingStyleRule
(que representa a regra @starting-style
) em CSSStyleRule.cssRules
.
A partir do Chrome 130, as declarações não são mais elevadas, e você acaba com dois objetos CSSRule
aninhados em CSSStyleRule.cssRules
. Em ordem: uma CSSStartingStyleRule
(que representa a regra @starting-style
) e uma CSSNestedDeclarations
que contém as declarações opacity: 1; scale: 1;
.
Devido a essa mudança de comportamento, as declarações @starting-style
são substituídas pelas contidas na instância CSSNestedDeclarations
, removendo a animação de entrada.
Para corrigir o código, verifique se o bloco @starting-style
vem depois das declarações regulares. Assim:
/* This works in Chrome 130 */
#mypopover:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.5;
}
}
Se você mantiver suas declarações aninhadas acima das regras aninhadas ao usar o aninhamento de CSS, seu código vai funcionar na maioria das vezes com todas as versões de todos os navegadores compatíveis com o aninhamento de CSS.
Por fim, se você quiser detectar o recurso disponível de CSSNestedDeclarations
, use o seguinte snippet de JavaScript:
if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
// CSSNestedDeclarations is not available
}