Dentro do polyfill da consulta do contêiner

Gerald Monaco
Gerald Monaco

As consultas de contêiner são um novo recurso de CSS que permite criar uma lógica de estilo que segmenta elementos de um elemento pai (por exemplo, largura ou altura) para estilizar os filhos dele. Recentemente, uma grande atualização no polyfill foi lançada, coincidindo com o suporte nos navegadores.

Nesta postagem, você vai saber como o polyfill funciona, os desafios que ele supera e as práticas recomendadas para oferecer uma ótima experiência do usuário aos visitantes.

Configurações avançadas

Transcompilação

Quando o analisador de CSS em um navegador encontra uma regra desconhecida, como a nova regra @container, ele simplesmente a descarta como se ela nunca existisse. Portanto, a primeira e mais importante coisa que o polyfill precisa fazer é transcompilar uma consulta @container em algo que não será descartado.

A primeira etapa da transpilação é converter a regra @container de nível superior em uma consulta @media. Isso basicamente garante que o conteúdo permaneça agrupado. Por exemplo, ao usar APIs CSSOM e visualizar a fonte CSS.

Antes
@container (width > 300px) {
  /* content */
}
Depois
@media all {
  /* content */
}

Antes das consultas de contêiner, o CSS não tinha como um autor ativar ou desativar arbitrariamente grupos de regras. Para aplicar o polyfill a esse comportamento, as regras dentro de uma consulta de contêiner também precisam ser transformadas. Cada @container recebe o próprio ID exclusivo (por exemplo, 123), que é usado para transformar cada seletor de modo que ele seja aplicado somente quando o elemento tiver um atributo cq-XYZ, incluindo esse ID. Esse atributo é definido pelo polyfill no tempo de execução.

Antes
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Depois
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Observe o uso da pseudoclasse :where(...). Normalmente, incluir mais um seletor de atributo aumenta a especificidade dele. Com a pseudoclasse, a condição extra pode ser aplicada, preservando a especificidade original. Para entender por que isso é importante, considere o exemplo a seguir:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

Com esse CSS, um elemento com a classe .card precisa sempre ter color: red, já que a regra posterior sempre vai substituir a regra anterior com o mesmo seletor e especificidade. A transpilação da primeira regra e a inclusão de um seletor de atributo extra sem :where(...) aumentaria a especificidade e faria com que color: blue fosse aplicada incorretamente.

No entanto, a pseudoclasse :where(...) é bem nova. Para navegadores que não têm suporte, o polyfill oferece uma solução alternativa segura e fácil: aumentar intencionalmente a especificidade das regras adicionando manualmente um seletor :not(.container-query-polyfill) fictício às regras @container:

Antes
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
Depois
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

Essa mudança tem vários benefícios:

  • O seletor no CSS de origem mudou, então a diferença na especificidade é explicitamente visível. Isso também funciona como uma documentação para que você saiba o que é afetado quando não precisar mais oferecer suporte à solução alternativa ou ao polyfill.
  • A especificidade das regras será sempre a mesma, já que o polyfill não a altera.

Durante a transcompilação, o polyfill substitui esse modelo pelo seletor de atributos com a mesma especificidade. Para evitar surpresas, o polyfill usa os dois seletores: o seletor de origem original é usado para determinar se o elemento precisa receber o atributo polyfill e o seletor transcompilado é usado para estilização.

Pseudoelementos

Uma pergunta que você pode se fazer é: se o polyfill define algum atributo cq-XYZ em um elemento para incluir o ID de contêiner exclusivo 123, como os pseudoelementos, que não podem ter atributos definidos neles, podem ser compatíveis?

Os pseudoelementos são sempre vinculados a um elemento real no DOM, chamado de elemento de origem. Durante a transcompilação, o seletor condicional é aplicado a esse elemento real:

Antes
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
Depois
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

Em vez de ser transformado em #foo::before:where([cq-XYZ~="123"]) (o que seria inválido), o seletor condicional é movido para o final do elemento de origem, #foo.

No entanto, isso não é tudo que é necessário. Um contêiner não pode modificar nada que esteja dentro dele (e um contêiner não pode estar dentro de si mesmo), mas considere que isso é exatamente o que aconteceria se #foo fosse o próprio elemento do contêiner que está sendo consultado. O atributo #foo[cq-XYZ] seria mudado de forma incorreta, e todas as regras #foo seriam aplicadas de maneira incorreta.

Para corrigir isso, o polyfill usa dois atributos: um que só pode ser aplicado a um elemento por um pai e outro que um elemento pode aplicar a si mesmo. O último atributo é usado para seletores que visam pseudoelementos.

Antes
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
Depois
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Como um contêiner nunca aplica o primeiro atributo (cq-XYZ-A) a si mesmo, o primeiro seletor só corresponderá se um contêiner pai diferente tiver atendido às condições do contêiner e o aplicado.

Unidades relativas do contêiner

As consultas de contêiner também vêm com algumas unidades novas que podem ser usadas no CSS, como cqw e cqh para 1% da largura e altura (respectivamente) do contêiner pai apropriado mais próximo. Para isso, a unidade é transformada em uma expressão calc(...) usando propriedades personalizadas de CSS. O polyfill definirá os valores dessas propriedades por meio de estilos in-line no elemento do contêiner.

Antes
.card {
  width: 10cqw;
  height: 10cqh;
}
Depois
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Também há unidades lógicas, como cqi e cqb, para o tamanho inline e do bloco (respectivamente). Eles são um pouco mais complicados, porque os eixos inline e de bloco são determinados pelo writing-mode do elemento que usa a unidade, não pelo elemento que está sendo consultado. Para oferecer suporte a esse recurso, o polyfill aplica um estilo in-line a qualquer elemento em que o writing-mode seja diferente do pai.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

Agora, as unidades podem ser transformadas na propriedade personalizada CSS apropriada, assim como antes.

Propriedades

As consultas de contêiner também adicionam algumas novas propriedades CSS, como container-type e container-name. Como APIs como getComputedStyle(...) não podem ser usadas com propriedades desconhecidas ou inválidas, elas também são transformadas em propriedades personalizadas de CSS após a análise. Se não for possível analisar uma propriedade (por exemplo, caso ela contenha um valor inválido ou desconhecido), ela será simplesmente deixada sozinha para o navegador.

Antes
.card {
  container-name: card-container;
  container-type: inline-size;
}
Depois
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Essas propriedades são transformadas sempre que são descobertas, permitindo que o polyfill funcione bem com outros recursos do CSS, como @supports. Essa funcionalidade é a base das práticas recomendadas para o uso do polyfill, conforme abordado abaixo.

Antes
@supports (container-type: inline-size) {
  /* ... */
}
Depois
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Por padrão, as propriedades personalizadas de CSS são herdadas, o que significa, por exemplo, que qualquer filha de .card assume o valor de --cq-XYZ-container-name e --cq-XYZ-container-type. Não é assim que as propriedades nativas se comportam. Para resolver isso, o polyfill insere a regra a seguir antes dos estilos de usuário, garantindo que cada elemento receba os valores iniciais, a menos que seja substituído intencionalmente por outra regra.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

Práticas recomendadas

É esperado que a maioria dos visitantes comece logo a usar navegadores com suporte para as consultas de contêineres integrados, mas, ainda assim, é importante que os visitantes restantes tenham uma boa experiência.

Durante o carregamento inicial, muita coisa precisa acontecer antes que o polyfill possa definir o layout da página:

  • O polyfill precisa ser carregado e inicializado.
  • As folhas de estilo precisam ser analisadas e transcompiladas. Como não há APIs para acessar a fonte bruta de uma folha de estilo externa, pode ser necessário buscá-la de novo de maneira assíncrona, embora o ideal seja fazer apenas a partir do cache do navegador.

Se essas preocupações não forem abordadas com cuidado pelo polyfill, suas Core Web Vitals poderão regredir.

Para oferecer uma experiência agradável aos visitantes, o polyfill foi projetado para priorizar a latência na primeira entrada (FID) e a mudança de layout cumulativa (CLS, na sigla em inglês), possivelmente à custa da Maior exibição de conteúdo (LCP). Em suma, o polyfill não garante que as consultas do contêiner serão avaliadas antes da primeira exibição. Isso significa que, para oferecer a melhor experiência ao usuário, você precisa garantir que qualquer conteúdo cujo tamanho ou posição seja afetado pelo uso de consultas de contêiner fique oculto até que o polyfill seja carregado e transcompilado seu CSS. Uma maneira de fazer isso é usando uma regra @supports:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

Recomenda-se que você combine isso com uma animação de carregamento de CSS pura, posicionada totalmente sobre seu conteúdo (oculto), para informar ao visitante que algo está acontecendo. Confira uma demonstração completa dessa abordagem aqui.

Essa abordagem é recomendada por vários motivos:

  • Um carregador de CSS puro minimiza a sobrecarga para usuários com navegadores mais recentes, enquanto fornece feedback leve para usuários de navegadores mais antigos e redes mais lentas.
  • Ao combinar o posicionamento absoluto do carregador com o visibility: hidden, você evita a mudança de layout.
  • Depois que o polyfill for carregado, a condição @supports deixará de ser transmitida, e seu conteúdo será revelado.
  • Em navegadores com suporte integrado para consultas de contêiner, a condição nunca será transmitida, por isso a página será exibida na primeira exibição, conforme o esperado.

Conclusão

Se você quiser usar consultas de contêiner em navegadores mais antigos, teste o polyfill. Não hesite em informar um problema se tiver algum problema.

Mal podemos esperar para conhecer e conhecer as coisas incríveis que você vai criar com ele.

Agradecimentos

Imagem hero de Dan Cristian P Badureğ no Unsplash.