Dentro do polyfill da consulta do contêiner

Gerald Monaco
Gerald Monaco

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

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

Configurações avançadas

Transpilação

Quando o analisador de CSS em um navegador encontra uma regra at-rule desconhecida, como a regra @container totalmente nova, ele a descarta como se ela nunca tivesse existido. Portanto, a primeira e mais importante coisa que o polyfill precisa fazer é transpilar 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 garante, principalmente, que o conteúdo permaneça agrupado. Por exemplo, ao usar APIs CSSOM e ao visualizar a origem do CSS.

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

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

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

Observe o uso da pseudoclasse :where(...). Normalmente, incluir um seletor de atributo adicional aumentaria a especificidade do seletor. Com a pseudoclasse, a condição extra pode ser aplicada enquanto preserva a especificidade original. Para entender por que isso é crucial, considere o exemplo a seguir:

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

.card {
  color: red;
}

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

No entanto, a pseudoclasse :where(...) é bastante nova. Para navegadores que não oferecem suporte, o polyfill oferece uma solução alternativa segura e fácil: é possível 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;
}

Isso tem vários benefícios:

  • O seletor no CSS de origem mudou, então a diferença na especificidade está explicitamente visível. Isso também funciona como 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 sempre será a mesma, já que o polyfill não a altera.

Durante a transpilação, o polyfill vai substituir esse valor fictício pelo seletor de atributo 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 transpilado é usado para estilizar.

Pseudoelementos

Uma pergunta a se fazer é: se o polyfill definir algum atributo cq-XYZ em um elemento para incluir o ID exclusivo do contêiner 123, como será possível oferecer suporte aos pseudoelementos, que não podem ter atributos definidos?

Os pseudoelementos são sempre vinculados a um elemento real no DOM, chamado de elemento de origem. Durante a transpilação, o seletor condicional é aplicado a este 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 o que é necessário. Um contêiner não pode modificar nada que não 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 elemento do contêiner sendo consultado. O atributo #foo[cq-XYZ] seria alterado incorretamente, e as regras #foo seriam aplicadas por engano.

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 segmentam 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 vai aplicar o primeiro atributo (cq-XYZ-A) a si mesmo, o primeiro seletor só vai corresponder se um contêiner pai diferente tiver atendido às condições do contêiner e o tiver aplicado.

Unidades relativas ao 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 mais próximo. Para oferecer suporte a eles, a unidade é transformada em uma expressão calc(...) usando propriedades personalizadas de CSS. O polyfill vai definir os valores dessas propriedades usando estilos inline 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);
}

Há também unidades lógicas, como cqi e cqb para tamanho inline e tamanho de bloco (respectivamente). Isso é um pouco mais complicado, porque os eixos inline e de bloco são determinados pelo writing-mode do elemento que usa a unidade, não do elemento que está sendo consultado. Para que isso aconteça, o polyfill aplica um estilo in-line a qualquer elemento cujo 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 de CSS adequada, 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 do CSS depois de serem analisadas. Se uma propriedade não puder ser analisada (por exemplo, porque contém um valor inválido ou desconhecido), ela simplesmente será deixada de lado para que o navegador a processe.

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 usar o polyfill, conforme descrito abaixo.

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

Por padrão, as propriedades personalizadas do CSS são herdadas, o que significa, por exemplo, que qualquer filho de .card vai assumir o valor de --cq-XYZ-container-name e --cq-XYZ-container-type. Esse não é o comportamento das propriedades nativas. Para resolver isso, o polyfill vai inserir a regra a seguir antes de qualquer estilo do usuário, garantindo que todos os elementos recebam os valores iniciais, a menos que sejam intencionalmente substituídos por outra regra.

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

Práticas recomendadas

Embora seja esperado que a maioria dos visitantes use navegadores com suporte integrado a consultas de contêiner, é importante oferecer uma boa experiência aos visitantes restantes.

Durante o carregamento inicial, muitas coisas precisam acontecer antes que o polyfill possa fazer o layout da página:

  • O polyfill precisa ser carregado e inicializado.
  • As folhas de estilo precisam ser analisadas e transpiladas. Como não há APIs para acessar a fonte bruta de uma folha de estilo externa, talvez seja necessário refazer a busca de forma assíncrona, embora o ideal seja 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 causar uma regressão.

Para facilitar a experiência dos visitantes, o polyfill foi projetado para priorizar o First Input Delay (FID) e o Cumulative Layout Shift (CLS), possivelmente em detrimento do Largest Contentful Paint (LCP). Especificamente, o polyfill não garante que as consultas de contêiner sejam avaliadas antes da first paint. Isso significa que, para oferecer a melhor experiência ao usuário, você precisa garantir que todo conteúdo com tamanho ou posição afetado pelo uso de consultas de contêiner fique oculto até que o polyfill tenha carregado e transcompilado o CSS. Uma maneira de fazer isso é usando uma regra @supports:

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

É recomendável combinar isso com uma animação de carregamento do CSS puro, posicionada de forma absoluta sobre o conteúdo (oculto), para informar ao visitante que algo está acontecendo. Você encontra 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, além de fornecer feedback leve para usuários com navegadores mais antigos e redes mais lentas.
  • Ao combinar o posicionamento absoluto do carregador com visibility: hidden, você evita a mudança de layout.
  • Depois que o polyfill for carregado, essa condição @supports deixará de ser transmitida, e seu conteúdo será revelado.
  • Em navegadores com suporte integrado a consultas de contêiner, a condição nunca será atendida e, portanto, a página será exibida na primeira pintura, como esperado.

Conclusão

Se você tem interesse em usar consultas de contêiner em navegadores mais antigos, experimente o polyfill. Envie um problema se tiver alguma dúvida.

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