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 que o conteúdo permaneça agrupado. Por exemplo, ao usar APIs CSSOM e ao visualizar a origem do CSS.
@container (width > 300px) { /* content */ }
@media all { /* content */ }
Antes das consultas de contêiner, o CSS não tinha uma maneira de um autor ativar ou desativar grupos de regras de forma arbitrária. Para polimorfizar esse comportamento, as regras dentro de 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.
@container (width > 300px) { .card { /* ... */ } }
@media all { .card:where([cq-XYZ~="123"]) { /* ... */ } }
Observe o uso da pseudoclasse :where(...)
. Normalmente, incluir um seletor de atributo adicional aumenta a especificidade do seletor. Com a pseudoclasse, a condição extra pode ser aplicada preservando 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
:
@container (width > 300px) { .card { color: blue; } } .card { color: red; }
@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 será sempre 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 que você pode fazer é: se o polyfill define algum atributo cq-XYZ
em um elemento para incluir o ID exclusivo do contêiner 123
, como os pseudoelementos, que não podem ter atributos definidos, podem ser compatíveis?
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:
@container (width > 300px) { #foo::before { /* ... */ } }
@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 todas as regras #foo
seriam aplicadas incorretamente.
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.
@container (width > 300px) { #foo, #foo::before { /* ... */ } }
@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 do CSS. O polyfill vai definir os valores dessas propriedades usando estilos inline no elemento do contêiner.
.card { width: 10cqw; height: 10cqh; }
.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 oferecer suporte a isso, o polyfill aplica um estilo inline a qualquer elemento em que 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 para o navegador processar.
.card { container-name: card-container; container-type: inline-size; }
.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.
@supports (container-type: inline-size) { /* ... */ }
@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 insere 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êineres, é 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 fazer uma nova busca assíncrona, mas o ideal é que isso seja feito apenas no cache do navegador.
Se essas questões não forem tratadas com cuidado pelo polyfill, isso poderá prejudicar suas Core Web Vitals.
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 do usuário, é necessário 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 carregue e transpile seu 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 do CSS puro, posicionada de forma absoluta sobre o 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, 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
vai parar de ser transmitida, e o conteúdo será revelado. - Em navegadores com suporte integrado a consultas de contêiner, a condição nunca será atendida, e a página será exibida na primeira pintura, como esperado.
Conclusão
Se você quiser usar consultas de contêiner em navegadores mais antigos, experimente o polyfill. Não hesite em enviar um problema se encontrar algum.
Mal podemos esperar para conferir as coisas incríveis que você vai criar com ele.