Nomes CSS definidos pelo autor e shadow DOM: na especificação e na prática

Os nomes de CSS definidos pelo autor e o DOM secundário devem funcionar juntos. No entanto, os navegadores são inconsistentes com a especificação, às vezes com cada outro, e cada nome de CSS é inconsistente de uma maneira um pouco diferente.

Este artigo documenta o status atual de como os nomes de CSS definidos pelo autor se comportam em escopos de sombra, com a esperança de que ele possa servir como um guia para melhorar a interoperabilidade em um futuro próximo.

O que são nomes de CSS definidos pelo autor?

Os nomes de CSS definidos pelo autor são um mecanismo de sintaxe de CSS relativamente antigo, originalmente introduzido para a regra @keyframes, que define um <keyframe-name> como um identificador personalizado ou uma string. O objetivo desse conceito é declarar algo em uma parte de uma folha de estilo e fazer referência a ele em outra parte.

/* "fade-in" is a CSS name, representing a set of keyframes */
@keyframes fade-in {
  from { opacity: 0 };
  to { opacity: 1 }
}

.card {
  /* "fade-in" is a reference to the above keyframes */
  animation-name: fade-in;
}

Outros recursos do CSS que usam nomes CSS são fontes, declarações de propriedade, consultas de contêiner e, mais recentemente, transições de visualização, posicionamento de âncora e animações movidas por rolagem. A tabela a seguir, não abrangente, inclui nomes que o Chrome verifica o estado.

Recurso Declaração de nome Referência de nome
Frames-chave @keyframes animation-name
Fontes @font-face { }
@font-palette-values
font-family
font-palette
Declarações de propriedade @property
Qualquer declaração de propriedade personalizada não registrada
var()
Conferir transições view-transition-name
view-transition-class
Pseudoelementos ::view-transition-*
Posicionamento da âncora anchor-name position-anchor
Animação gerada por rolagem view-timeline-name
scroll-timeline-name
animation-timeline
Estilos de lista @counter-style list-style
Contadores counter-reset
counter-set
counter-increment
Consultas em contêiner container-name @container
Página page @page

Como você pode ver na tabela, um nome CSS geralmente tem uma referência CSS correspondente. Por exemplo, animation-name é uma referência ao nome @keyframes. Os nomes CSS são diferentes dos nomes definidos no DOM, como atributos e nomes de tags, porque são declarados e referenciados no contexto das folhas de estilo.

Como os nomes se relacionam com o shadow DOM

Enquanto os nomes CSS são criados para criar relações entre diferentes partes de um documento ou folha de estilo, o DOM sombra é criado para fazer o oposto. Ele encapsula relacionamentos para que eles não vazem em componentes da Web que devem ter o próprio namespace.

Ao unir nomes de CSS e o DOM de sombra, a experiência de composição de componentes da Web precisa ser suficientemente flexível para ser flexível, mas restrita o suficiente para ser estável.

Isso é bom em teoria. Na prática, os navegadores são inconsistentes na forma como os nomes CSS interagem com o DOM sombra, tanto entre recursos no mesmo navegador quanto entre navegadores e entre os recursos e a especificação.

Como os nomes e o DOM sombra precisam funcionar juntos

Para entender o problema, é importante entender como essas partes do CSS devem funcionar juntas em teoria.

A regra geral

A regra geral sobre como os nomes do CSS se comportam nas árvores de sombra é definida na especificação do nível 1 do escopo do CSS. Resumindo: um nome de CSS é global dentro do escopo em que é definido, ou seja, ele pode ser acessado de árvores shadow descendentes, mas não de árvores shadow irmãs ou ancestral. Isso é diferente dos nomes na plataforma da Web, como IDs de elementos, que são encapsulados no mesmo escopo de árvore.

Exceção à regra: @property

Ao contrário de outros nomes de CSS, as propriedades de CSS não são encapsuladas pelo Shadow DOM. Em vez disso, elas são o meio comum de transmitir parâmetros em diferentes árvores de sombra. Isso torna o descritor @property especial: ele precisa se comportar como uma declaração de tipo global do documento que define como uma propriedade nomeada específica age. Como as propriedades precisam ser correspondentes em árvores de sombra, a incompatibilidade de declarações de propriedade criaria resultados inesperados. Portanto, as declarações de @property são especificadas para serem achatadas e resolvidas de acordo com a ordem do documento.

Como a regra funciona com ::part

As partes de sombra expõem um elemento dentro de uma árvore de sombra para a árvore mãe. Ao fazer isso, a árvore pai pode acessar esse elemento e também estilizá-lo usando o elemento ::part.

Como ::part permite que dois escopos de árvore estilizem o mesmo elemento, a seguinte ordem em cascata é especificada:

  1. Primeiro, verifique o estilo dentro do contexto da sombra. Esse é o estilo "padrão" da parte.
  2. Em seguida, aplique o estilo externo conforme definido em ::part. Esse é o estilo "personalizado" da parte.
  3. Em seguida, aplique qualquer estilo interno definido com !important. Isso permite que um elemento personalizado declare que uma determinada propriedade de uma determinada parte não é personalizável por ::part.

Isso significa que os nomes do DOM shadow não podem ser referenciados por um ::part, já que o ::part é um estilo com escopo de host, e não um estilo com escopo shadow. Exemplo:

// inside the shadow DOM:
@keyframes fade-in {
  from { opacity: 0}
}

// This shouldn't work!
// The host style shouldn't know the name "fade-in"
::part(slider) {
  animation-name: fade-in;  
}

Como a regra funciona com estilos inline

Ao contrário do ::part, os estilos inline com o atributo style ou aqueles que definem o estilo de forma programática usando o script têm o mesmo escopo do elemento. Isso acontece porque, para aplicar um estilo a um elemento, é necessário ter acesso ao identificador do elemento e, portanto, à raiz paralela.

Como os nomes de CSS e o DOM sombra funcionam juntos na prática

Embora as regras anteriores sejam bem definidas e consistentes, as implementações atuais nem sempre refletem isso. Na prática, o @property funciona de maneira diferente da especificação de forma consistente em todos os navegadores, e a maioria dos outros recursos tem bugs abertos (alguns deles ainda não foram lançados, então há tempo para corrigi-los).

Para testar e demonstrar como esses recursos funcionam na prática, criamos a página a seguir: https://css-names-in-the-shadow.glitch.me/. Esta página tem vários iframes, cada um focado em um dos recursos e testando seis cenários:

  • Referência externa a um nome externo: sem DOM externo envolvido, isso deve funcionar.
  • Referência externa a um nome interno: isso não funciona, porque significaria que o nome definido no contexto sombra vazou.
  • Referência interna ao nome externo: isso deve funcionar, já que os nomes no escopo da árvore são herdados pelas raízes de sombra.
  • Referência interna ao nome interno: isso deve funcionar, já que o nome da referência está no mesmo escopo.
  • Referência de ::part ao nome externo: isso deve funcionar, já que o ::part e o nome são declarados no mesmo escopo.
  • Referência ::part ao nome interno: isso não funciona, porque o escopo externo não tem conhecimento sobre os nomes declarados dentro do shadow DOM.

@keyframes

Conforme definido na especificação, você pode referenciar nomes de frames-chave em uma raiz de sombra, desde que a regra @keyframes at-rule esteja em um escopo ancestral. Na prática, nenhum navegador implementa esse comportamento, e as definições de frame-chave só podem ser referenciadas no escopo em que são definidas. Consulte o problema 10540.

@property

Conforme definido na especificação, qualquer declaração de @property será agrupada no escopo do documento. No entanto, atualmente, em todos os navegadores, só é possível declarar @property no escopo do documento, e as declarações de @property dentro de raízes de sombra são ignoradas.
Consulte o problema 10541.

Bugs específicos do navegador

Os outros recursos não mostram um comportamento consistente em todos os navegadores:

  • @font-face é achatado para o escopo raiz no Safari.
  • O Chromium não permite a herança de regras anchor-name em uma raiz sombra
  • scroll-timeline-name e view-timeline-name não têm o escopo correto em ::part (também no Chromium).
  • Nenhum navegador permite declarar @font-palette-values em uma raiz de sombra.
  • view-transition-class pode ser definido dentro de uma raiz sombra (a transição está fora da raiz sombra).
  • O Firefox permite que ::part acesse nomes de sombra internos (consultas de contêiner, keyframes).
  • O Firefox e o Safari não respeitam @counter-style em uma raiz de sombra.

counter-reset, counter-set e counter-increment têm regras um pouco diferentes porque são nomes implícitos, e a declaração de propriedades CSS tem um conjunto de regras estabelecido e bem testado.

Conclusão

A má notícia é que, ao examinar o instantâneo do estado de interoperabilidade atual em relação aos nomes do CSS e ao DOM sombra, a experiência é inconsistente e com bugs. Nenhum dos recursos examinados aqui se comporta de maneira consistente em navegadores e de acordo com as especificações. A boa notícia é que o delta para tornar a experiência consistente é uma lista de bugs e problemas de especificação finita. Vamos corrigir isso. Enquanto isso, este resumo pode ajudar se você estiver com dificuldades com as inconsistências descritas neste artigo.