Dentro do polyfill da consulta do contêiner

Gerald Monaco
Gerald Monaco

As consultas de contêiner são um novo recurso CSS que permite escrever uma lógica de estilo que direciona os elementos de um elemento pai (por exemplo, largura ou altura) para estilizar os filhos. Recentemente, lançamos uma grande atualização para o polyfill (links em inglês), que coincide com a chegada ao suporte em navegadores.

Nesta postagem, você poderá dar uma olhada em como o polyfill funciona, os desafios superados e as práticas recomendadas para proporcionar uma ótima experiência do usuário aos seus visitantes.

Configurações avançadas

Transpilação

Quando o analisador de CSS dentro de um navegador encontra uma regra desconhecida, como a nova regra @container, ele simplesmente a descarta como se 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 transcompilaçã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 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 o próprio ID exclusivo (por exemplo, 123), que é usado para transformar cada seletor de modo que ele só seja aplicado quando o elemento tiver um atributo cq-XYZ que inclua esse ID. Esse atributo será 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 um seletor de atributo adicional aumentaria a especificidade dele. 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;
}

Considerando esse CSS, um elemento com a classe .card precisa sempre ter color: red, já que a regra posterior sempre vai substituir a anterior com o mesmo seletor e especificidade. Transcompilar a primeira regra e incluir um seletor de atributo adicional sem :where(...) aumentaria a especificidade e faria com que color: blue fosse aplicado de maneira equivocada.

No entanto, a pseudoclasse :where(...) é bastante nova. Para navegadores que não são compatíveis com ela, o polyfill oferece uma solução segura e fácil: é possível aumentar intencionalmente a especificidade das regras adicionando manualmente um seletor :not(.container-query-polyfill) fictício às regras do @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 traz vários benefícios:

  • O seletor no CSS de origem foi alterado, portanto, a diferença em especificidade é explicitamente visível. Isso também serve como documentação para que você saiba o que será afetado quando não precisar mais de suporte para a solução alternativa ou para o polyfill.
  • A especificidade das regras sempre será a mesma, já que o polyfill não a altera.

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

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 estã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 fim do elemento de origem, #foo.

No entanto, isso não é tudo o que precisamos. Um contêiner não tem permissão para modificar qualquer coisa que não esteja contida nele (e um contêiner não pode estar dentro de si mesmo). No entanto, isso é exatamente o que aconteceria se #foo fosse o próprio elemento de contêiner que está 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 fazer a correspondência se um contêiner pai diferente tiver atendido às condições e aplicado o contêiner.

Unidades relativas do contêiner

As consultas de contêiner também vêm com algumas novas unidades que podem ser usadas no seu CSS, como cqw e cqh para 1% da largura e altura (respectivamente) do contêiner pai apropriado mais próximo. Para oferecer suporte a eles, a unidade é transformada em uma expressão calc(...) usando propriedades personalizadas de CSS. O polyfill definirá os valores para essas propriedades por meio de estilos in-line no elemento de 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 tamanho inline e tamanho do bloco (respectivamente). Elas são um pouco mais complicadas, porque os eixos inline e de bloco são determinados pelo writing-mode do elemento que usa a unidade, e não pelo elemento que está sendo consultado. Para isso, 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 do CSS apropriada como antes.

Propriedades

As consultas de contêiner também adicionam algumas propriedades CSS novas, 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 serem analisadas. Se não for possível analisar uma propriedade (por exemplo, se ela tiver um valor inválido ou desconhecido), ela será deixada sozinha para o navegador processar.

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 de CSS, como @supports. Essa funcionalidade é a base das práticas recomendadas de 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 do CSS são herdadas. Isso significa, por exemplo, que qualquer filho de .card receberá o valor de --cq-XYZ-container-name e --cq-XYZ-container-type. Definitivamente não é assim que as propriedades nativas se comportam. Para resolver isso, o polyfill insere a regra a seguir antes de qualquer estilo 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 execute navegadores com suporte integrado para consultas de contêiner o quanto antes, mas ainda é importante oferecer aos demais visitantes uma boa experiência.

Durante o carregamento inicial, muita coisa precisa acontecer antes que o polyfill crie 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, talvez seja necessário realizar uma nova busca de forma assíncrona, embora idealmente 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 oferecer uma experiência agradável aos visitantes, o polyfill foi desenvolvido para priorizar First Input Delay (FID) e Cumulative Layout Shift (CLS), possivelmente à custa da Largest Contentful Paint (LCP). Concretamente, o polyfill não garante que as consultas de contêiner serão 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;
  }
}

Recomendamos que você combine isso com uma animação de carregamento CSS pura, posicionada de forma absoluta sobre seu 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 CSS puro minimiza a sobrecarga para usuários com navegadores mais recentes, ao mesmo tempo que fornece 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 para consultas de contêiner, a condição nunca será aprovada e, portanto, a página será exibida na first-paint conforme esperado.

Conclusão

Se você tem interesse em usar consultas de contêiner em navegadores mais antigos, experimente o polyfill. Não hesite em registrar um problema se você tiver algum problema.

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

Agradecimentos

Imagem hero de Dan Cristian Pødurerpc no Unsplash.