Como usar consultas de contêiner agora

Recentemente, Chris Coyier escreveu uma postagem do blog fazendo a seguinte pergunta:

Agora que as consultas de contêiner têm suporte em todos os mecanismos de navegador, por que mais desenvolvedores não as usam?

A postagem de Caio lista vários possíveis motivos (por exemplo, falta de consciência e hábitos antigos que saíram muito), mas há um motivo particular que se destaca.

Alguns desenvolvedores dizem que querem usar consultas de contêiner agora, mas acham que não podem porque ainda precisam oferecer suporte a navegadores mais antigos.

Como você deve ter adivinhado, a maioria dos desenvolvedores pode usar consultas de contêiner agora, em produção, mesmo que você tenha que oferecer suporte a navegadores mais antigos. Esta postagem explica a abordagem recomendada para fazer isso.

Uma abordagem pragmática

Se você quiser usar consultas de contêiner no seu código agora, mas quiser que a experiência tenha a mesma aparência em todos os navegadores, implemente um substituto baseado em JavaScript para navegadores que não oferecem suporte a esse tipo de consulta.

A pergunta passa a ser: qual deve ser o nível de abrangência do substituto?

Como em qualquer substituição, o desafio é encontrar um bom equilíbrio entre utilidade e desempenho. Para recursos CSS, geralmente é impossível oferecer suporte à API completa. Consulte por que não usar um polyfill. No entanto, você pode ir longe identificando o conjunto principal de funcionalidades que a maioria dos desenvolvedores quer usar e, em seguida, otimizar o substituto apenas para esses recursos.

Mas qual é o "conjunto principal de funcionalidades" que a maioria dos desenvolvedores quer para as consultas de contêiner? Para responder a essa pergunta, considere como a maioria dos desenvolvedores cria sites responsivos atualmente com consultas de mídia.

Quase todos os sistemas de design modernos e bibliotecas de componentes padronizaram os princípios que priorizam dispositivos móveis, implementados usando um conjunto de pontos de interrupção predefinidos (como SM, MD, LG e XL). Os componentes são otimizados para ter uma boa exibição em telas pequenas por padrão. Em seguida, os estilos são condicionalmente sobrepostos para oferecer suporte a um conjunto fixo de larguras de tela maiores. Consulte as documentações Inicialização e Tailwind para ver exemplos.

Essa abordagem é tão relevante para os sistemas de design baseados em contêiner quanto para os sistemas de design baseados em janela de visualização, porque, na maioria dos casos, o que é relevante para os designers não é o tamanho da tela ou da janela de visualização, mas sim quanto espaço está disponível para o componente no contexto em que ele foi colocado. Em outras palavras, em vez de serem relativos a toda a janela de visualização (e aplicáveis a toda a página), os pontos de interrupção se aplicariam a áreas de conteúdo específicas, como barras laterais, caixas de diálogo modais ou corpos de postagens.

Se você conseguir trabalhar dentro das restrições de uma abordagem baseada em pontos de interrupção e que prioriza dispositivos móveis (o que a maioria dos desenvolvedores faz atualmente), implementar um substituto baseado em contêiner para essa abordagem será muito mais fácil do que implementar o suporte completo para cada recurso de consulta de contêiner.

A próxima seção explica exatamente como tudo isso funciona, junto com um guia passo a passo que mostra como implementá-lo em um site existente.

Como funciona

Etapa 1: atualizar os estilos dos componentes para usar regras @container em vez de @media

Nesta primeira etapa, identifique os componentes do seu site que você acha que se beneficiariam do dimensionamento com base em contêiner em vez do dimensionamento com base na janela de visualização.

É uma boa ideia começar com apenas um ou dois componentes para ver como essa estratégia funciona, mas se você quiser converter 100% dos componentes em estilo baseado em contêiner, não tem problema. O melhor dessa estratégia é que você pode adotá-la de modo incremental, se necessário.

Depois de identificar os componentes que você quer atualizar, é necessário mudar todas as regras @media no CSS desses componentes para uma regra @container. É possível manter as mesmas condições de tamanho.

Se o CSS já estiver usando um conjunto de pontos de interrupção predefinidos, você poderá continuar a usá-los exatamente como estão definidos. Se você ainda não estiver usando pontos de interrupção predefinidos, precisará definir nomes para eles. Eles serão usados posteriormente em JavaScript. Consulte a etapa 2.

Confira um exemplo de estilos para um componente .photo-gallery que, por padrão, é uma única coluna. Em seguida, ele atualiza o estilo para se tornar duas e três colunas nos pontos de interrupção MD e XL (respectivamente):

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;
}

/* Styles for the `MD` breakpoint */
@media (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Styles for the `XL` breakpoint */
@media (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

Para mudar esses estilos de componentes de regras @media para @container, faça uma operação de localizar e substituir no seu código:

/* Before: */
@media (min-width: 768px) { /* ... */ }
@media (min-width: 1280px) { /* ... */ }

/* After: */
@container (min-width: 768px) { /* ... */ }
@container (min-width: 1280px) { /* ... */ }

Depois de atualizar os estilos dos componentes de regras @media para regras @container baseadas em pontos de interrupção, a próxima etapa é configurar os elementos do contêiner.

Etapa 2: adicione elementos de contêiner ao seu HTML

A etapa anterior definiu estilos de componentes com base no tamanho de um elemento de contêiner. A próxima etapa é definir quais elementos na sua página serão os elementos do contêiner cujo tamanho será relativo às regras de @container.

Para declarar qualquer elemento como contêiner no CSS, defina a propriedade container-type dele como size ou inline-size. Se as regras de contêiner forem baseadas na largura, então inline-size será geralmente o que você pretende usar.

Considere um site com a seguinte estrutura HTML básica:

<body>
  <div class="sidebar">...</div>
  <div class="content">...</div>
</body>

Para tornar os elementos .sidebar e .content desse site containers, adicione esta regra ao seu CSS:

.content, .sidebar {
  container-type: inline-size;
}

Para navegadores compatíveis com consultas de contêiner, esse CSS é tudo que você precisa para tornar os estilos dos componentes definidos na etapa anterior em relação à área de conteúdo principal ou à barra lateral, dependendo do elemento em que estão.

No entanto, para navegadores que não oferecem suporte a consultas de contêiner, há um trabalho adicional a ser feito.

Você precisa adicionar um código que detecte quando o tamanho dos elementos do contêiner muda e, em seguida, atualizar o DOM com base nessas alterações de uma forma que o CSS possa acessar.

Felizmente, o código necessário para fazer isso é mínimo e pode ser completamente abstraído em um componente compartilhado que pode ser usado em qualquer site e em qualquer área de conteúdo.

O código a seguir define um elemento <responsive-container> reutilizável que detecta mudanças de tamanho automaticamente e adiciona classes de ponto de interrupção que podem ser estilizadas pelo seu CSS:

// A mapping of default breakpoint class names and min-width sizes.
// Redefine these as needed based on your site's design.
const defaultBreakpoints = {SM: 512, MD: 768, LG: 1024, XL: 1280};

// A resize observer that monitors size changes to all <responsive-container>
// elements and calls their `updateBreakpoints()` method with the updated size.
const ro = new ResizeObserver((entries) => {
  entries.forEach((e) => e.target.updateBreakpoints(e.contentRect));
});

class ResponsiveContainer extends HTMLElement {
  connectedCallback() {
    const bps = this.getAttribute('breakpoints');
    this.breakpoints = bps ? JSON.parse(bps) : defaultBreakpoints;
    this.name = this.getAttribute('name') || '';
    ro.observe(this);
  }
  disconnectedCallback() {
    ro.unobserve(this);
  }
  updateBreakpoints(contentRect) {
    for (const bp of Object.keys(this.breakpoints)) {
      const minWidth = this.breakpoints[bp];
      const className = this.name ? `${this.name}-${bp}` : bp;
      this.classList.toggle(className, contentRect.width >= minWidth);
    }
  }
}

self.customElements.define('responsive-container', ResponsiveContainer);

Esse código cria um ResizeObserver que detecta automaticamente as mudanças de tamanho em qualquer elemento <responsive-container> no DOM. Se a mudança de tamanho corresponder a um dos tamanhos de ponto de interrupção definidos, uma classe com o nome desse ponto de interrupção será adicionada ao elemento (e removida se a condição não corresponder mais).

Por exemplo, se o width do elemento <responsive-container> estiver entre 768 e 1.024 pixels (com base nos valores de ponto de interrupção padrão definidos no código), as classes SM e MD serão adicionadas desta forma:

<responsive-container class="SM MD">...</responsive-container>

Essas classes permitem que você defina estilos substitutos para navegadores que não aceitam consultas de contêiner. Consulte a etapa 3: adicionar estilos substitutos ao CSS.

Para atualizar o código HTML anterior e usar esse elemento de contêiner, mude os elementos <div> da barra lateral e do conteúdo principal para <responsive-container>:

<body>
  <responsive-container class="sidebar">...</responsive-container>
  <responsive-container class="content">...</responsive-container>
</body>

Na maioria das situações, você pode apenas usar o elemento <responsive-container> sem nenhuma personalização, mas, se for necessário, as seguintes opções estão disponíveis:

  • Tamanhos de ponto de interrupção personalizados:esse código usa um conjunto de nomes de classe de ponto de interrupção padrão e tamanhos de largura mínima, mas você pode mudar esses padrões para o que quiser. Também é possível substituir esses valores por elemento usando o atributo breakpoints.
  • Contêineres nomeados:este código também aceita contêineres nomeados transmitindo um atributo name. Isso pode ser importante se você precisar aninhar elementos de contêiner. Consulte a seção de limitações para mais detalhes.

Veja um exemplo que define essas duas opções de configuração:

<responsive-container
  name='sidebar'
  breakpoints='{"bp1":500,"bp2":1000,"bp3":1500}'>
</responsive-container>

Por fim, ao agrupar esse código, use a detecção de recursos e o import() dinâmico para carregá-lo apenas se o navegador não oferecer suporte a consultas de contêiner.

if (!CSS.supports('container-type: inline-size')) {
  import('./path/to/responsive-container.js');
}

Etapa 3: adicionar estilos substitutos ao CSS

A última etapa nessa estratégia é adicionar estilos substitutos para navegadores que não reconhecem os estilos definidos nas regras @container. Para fazer isso, duplique essas regras usando as classes de ponto de interrupção definidas nos elementos <responsive-container>.

Continuando com o exemplo .photo-gallery anterior, os estilos substitutos das duas regras @container podem ficar assim:

/* Container query styles for the `MD` breakpoint. */
@container (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Fallback styles for the `MD` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.MD) .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Container query styles for the `XL` breakpoint. */
@container (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

/* Fallback styles for the `XL` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.XL) .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

Nesse código, para cada regra @container há uma regra equivalente que corresponde condicionalmente ao elemento <responsive-container> se a classe de ponto de interrupção correspondente estiver presente.

A parte do seletor que corresponde ao elemento <responsive-container> é agrupada em um seletor de pseudoclasse funcional :where() para manter a especificidade do seletor substituto equivalente à do seletor original dentro da regra @container.

Cada regra substituta também é incluída em uma declaração @supports. Embora isso não seja estritamente necessário para que o substituto funcione, significa que o navegador ignora completamente essas regras se oferecer suporte a consultas de contêiner, o que pode melhorar o desempenho da correspondência de estilo em geral. Ele também permite que ferramentas de build ou CDNs removam essas declarações se souberem que o navegador oferece suporte a consultas de contêiner e não precisa desses estilos substitutos.

A principal desvantagem dessa estratégia substituta é que ela exige que você repita a declaração de estilo duas vezes, o que é tedioso e propenso a erros. No entanto, se você estiver usando um pré-processador CSS, é possível abstrair isso em um mixin que gera a regra @container e o código substituto para você. Veja um exemplo que usa Sass:

@use 'sass:map';

$breakpoints: (
  'SM': 512px,
  'MD': 576px,
  'LG': 1024px,
  'XL': 1280px,
);

@mixin breakpoint($breakpoint) {
  @container (min-width: #{map.get($breakpoints, $breakpoint)}) {
    @content();
  }
  @supports not (container-type: inline-size) {
    :where(responsive-container.#{$breakpoint}) & {
      @content();
    }
  }
}

Em seguida, quando tiver esse mixin, atualize os estilos originais do componente .photo-gallery para algo assim, o que elimina completamente a duplicação:

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;

  @include breakpoint('MD') {
    grid-template-columns: 1fr 1fr;
  }

  @include breakpoint('XL') {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

Isso é tudo!

Recapitulação

Para recapitular, veja como atualizar seu código para usar consultas de contêiner agora com um substituto em vários navegadores.

  1. Os componentes de identidade que você quer estilizar em relação ao contêiner deles e atualizar as regras @media no CSS para usar as regras @container. Além disso, caso ainda não tenha feito isso, padronize um conjunto de nomes de ponto de interrupção para corresponder às condições de tamanho nas suas regras de contêiner.
  2. Adicione o JavaScript que alimenta o elemento <responsive-container> personalizado e, em seguida, adicione o elemento <responsive-container> a todas as áreas de conteúdo da página às quais você quer que os componentes sejam relacionados.
  3. Para oferecer suporte a navegadores mais antigos, adicione estilos substitutos ao CSS que correspondam às classes de ponto de interrupção que são adicionadas automaticamente aos elementos <responsive-container> no HTML. O ideal é usar um mixin de pré-processador CSS para não precisar escrever os mesmos estilos duas vezes.

A grande vantagem dessa estratégia é o custo de configuração única, mas não é necessário mais esforço para adicionar novos componentes e definir estilos relativos ao contêiner para eles.

Como vê-lo em ação

Provavelmente, a melhor maneira de entender como todas essas etapas se encaixam é assistindo uma demonstração dele em ação.

Um vídeo de um usuário interagindo com o site de demonstração de consultas de contêiner. O usuário está redimensionando as áreas de conteúdo para mostrar como os estilos dos componentes são atualizados com base no tamanho da área.

Esta demonstração é uma versão atualizada de um site criado em 2019 (antes de existirem as consultas de contêiner) para ajudar a ilustrar por que as consultas de contêiner são essenciais para criar bibliotecas de componentes realmente responsivas.

Como esse site já tinha estilos definidos para vários "componentes responsivos", ele era um candidato perfeito para testar a estratégia apresentada aqui em um site não trivial. Acontece que, na verdade, era muito simples de atualizar e quase não exigiu alterações nos estilos originais do site.

Confira o código-fonte da demonstração completo no GitHub e consulte especificamente o CSS do componente de demonstração para saber como os estilos substitutos são definidos. Se você quiser testar somente o comportamento do substituto, há uma demonstração somente substituto que inclui apenas essa variante, mesmo em navegadores compatíveis com consultas de contêiner.

Limitações e possíveis melhorias

Como mencionado no início desta postagem, a estratégia descrita aqui funciona bem para a maioria dos casos de uso com os quais os desenvolvedores se preocupam ao realizar consultas de contêiner.

Dito isso, há alguns casos de uso mais avançados que essa estratégia não tenta oferecer intencionalmente, que são abordados a seguir:

Unidades de consulta do contêiner

A especificação para consultas de contêiner define várias unidades novas, todas relacionadas ao tamanho do contêiner. Embora potencialmente úteis em alguns casos, a maioria dos designs responsivos provavelmente pode ser alcançada por meios atuais, como porcentagens ou layouts de grade ou flexíveis.

Dito isso, se você precisa usar unidades de consulta de contêiner, pode adicionar o suporte a elas facilmente usando as propriedades personalizadas. Especificamente, definindo uma propriedade personalizada para cada unidade usada no elemento de contêiner, desta forma:

responsive-container {
  --cqw: 1cqw;
  --cqh: 1cqh;
}

Assim, sempre que você precisar acessar as unidades de consulta de contêiner, use essas propriedades em vez de usar a unidade em si:

.photo-gallery {
  font-size: calc(10 * var(--cqw));
}

Em seguida, para oferecer suporte a navegadores mais antigos, defina os valores dessas propriedades personalizadas no elemento de contêiner dentro do callback ResizeObserver.

class ResponsiveContainer extends HTMLElement {
  // ...
  updateBreakpoints(contentRect) {
    this.style.setProperty('--cqw', `${contentRect.width / 100}px`);
    this.style.setProperty('--cqh', `${contentRect.height / 100}px`);

    // ...
  }
}

Isso permite que você "transmita" esses valores do JavaScript para o CSS. Assim, você tem todo o poder do CSS (por exemplo, calc(), min(), max(), clamp()) para manipulá-los conforme necessário.

Compatibilidade com propriedades lógicas e modo de gravação

Você deve ter notado o uso de inline-size em vez de width nas declarações @container em alguns desses exemplos de CSS. Você também pode ter notado as novas unidades cqi e cqb (para tamanhos inline e de bloco, respectivamente). Esses novos recursos refletem a mudança do CSS para propriedades e valores lógicos em vez de físicos ou direcionais.

Infelizmente, APIs como Resize Observer ainda informam valores em width e height. Portanto, se os designs precisarem da flexibilidade das propriedades lógicas, você vai precisar descobrir isso por conta própria.

Embora seja possível usar o modo de gravação usando algo como getComputedStyle() transmitindo o elemento do contêiner, isso tem um custo, e não há uma boa maneira de detectar se o modo de gravação muda.

Por esse motivo, a melhor abordagem é que o próprio elemento <responsive-container> aceite uma propriedade do modo de escrita que o proprietário do site possa definir e atualizar conforme necessário. Para implementar isso, siga a mesma abordagem mostrada na seção anterior e troque width e height conforme necessário.

Contêineres aninhados

A propriedade container-name permite dar um nome a um contêiner, que pode ser referenciado em uma regra @container. Os contêineres nomeados são úteis se você tiver contêineres aninhados em contêineres e precisar que certas regras correspondam apenas a determinados contêineres (não apenas o contêiner ancestral mais próximo).

A estratégia de fallback descrita aqui usa o combinador descendente para definir o estilo de elementos que correspondem a determinadas classes de ponto de interrupção. Isso poderá ser corrompido se você tiver contêineres aninhados, já que qualquer número de classes de ponto de interrupção de vários ancestrais de elementos de contêiner pode corresponder a um determinado componente ao mesmo tempo.

Por exemplo, aqui há dois elementos <responsive-container> envolvendo o componente .photo-gallery, mas, como o contêiner externo é maior que o interno, eles têm diferentes classes de ponto de interrupção adicionadas.

<responsive-container class="SM MD LG">
  ...
  <responsive-container class="SM">
    ...
    <div class="photo-gallery">...</div class="photo-gallery">
  </responsive-container>
</responsive-container>

Nesse exemplo, as classes MD e LG no contêiner externo afetariam as regras de estilo correspondentes ao componente .photo-gallery, que não corresponde ao comportamento das consultas do contêiner, já que elas correspondem apenas ao contêiner ancestral mais próximo.

Para lidar com isso:

  1. Sempre nomeie os contêineres que você estiver aninhando e verifique se as classes de ponto de interrupção estão prefixadas com esse nome de contêiner para evitar conflitos.
  2. Use o combinador filho em vez do descendente nos seletores de substituto (o que é um pouco mais limitador).

A seção de contêineres aninhados do site de demonstração tem um exemplo de funcionamento usando contêineres nomeados, junto com o mixin sass usado no código para gerar os estilos substitutos para regras @container com e sem nome.

E os navegadores que não oferecem suporte a :where(), elementos personalizados ou Resize Observer?

Embora essas APIs possam parecer relativamente novas, elas são compatíveis com todos os navegadores há mais de três anos e fazem parte do Baseline amplamente disponíveis.

Portanto, a menos que você tenha dados mostrando que uma parte significativa dos visitantes do seu site usa navegadores que não são compatíveis com um desses recursos, não há motivo para não usá-los livremente sem um substituto.

Mesmo assim, para esse caso de uso específico, o pior que pode acontecer é que o substituto não funcione para uma porcentagem muito pequena de usuários, o que significa que eles vão ver a visualização padrão em vez de uma otimizada para o tamanho do contêiner.

A funcionalidade do site ainda deve funcionar, e é isso que realmente importa.

Por que não usar apenas um polyfill de consulta de contêiner?

Os recursos CSS são notoriamente difíceis de polyfill e geralmente exigem a reimplementação do analisador CSS e da lógica em cascata do navegador novamente. Como resultado, os autores de polyfill de CSS precisam fazer muitas compensações que quase sempre trazem inúmeras limitações de recursos, além de uma sobrecarga de desempenho significativa.

Por esses motivos, geralmente não recomendamos o uso de polyfills CSS na produção, incluindo o container-query-polyfill do Google Chrome Labs, que não é mais mantido (e foi criado principalmente para fins de demonstração).

A estratégia substituta discutida aqui tem menos limitações, requer muito menos código e terá um desempenho significativamente melhor do que qualquer polyfill de consulta de contêiner poderia.

Você precisa mesmo implementar um substituto para navegadores mais antigos?

Se você está preocupado com alguma das limitações mencionadas aqui, provavelmente vale a pena se perguntar se você realmente precisa implementar uma substituição em primeiro lugar. Afinal, a maneira mais fácil de evitar essas limitações é simplesmente usar o recurso sem nenhum substituto. Honestamente, em muitos casos, essa pode ser uma escolha perfeitamente razoável.

De acordo com o site caniuse.com (link em inglês), 90% dos usuários do mundo todo aceitam as consultas de contêiner, e o número é bem maior para a base de usuários do mundo. Por isso, é importante lembrar que a maioria dos usuários vai acessar a versão de consulta de contêiner da sua interface. E para os 10% dos usuários que não vão ter uma experiência corrompida. Ao seguir essa estratégia, na pior das hipóteses, esses usuários verão o layout padrão ou para dispositivos móveis de alguns componentes, o que não é o fim do mundo.

Ao fazer compensações, é uma boa prática otimizar para a maioria dos usuários, em vez de adotar uma abordagem de menor denominador comum que ofereça a todos uma experiência consistente, mas inferior.

Portanto, antes de presumir que não é possível usar consultas de contêiner devido à falta de suporte ao navegador, pense em como seria a experiência se você as adotasse. A troca pode valer a pena, mesmo sem substitutos.

Para o futuro

Esperamos que esta postagem tenha convencido você de que é possível usar consultas de contêiner na produção agora e que não precisa esperar anos até que todos os navegadores incompatíveis desapareçam completamente.

Embora a estratégia descrita aqui exija um pouco de trabalho extra, ela deve ser simples e direta o suficiente para que a maioria das pessoas possa adotá-la em seus sites. Dito isso, certamente há espaço para facilitar ainda mais a adoção. Uma ideia seria consolidar muitas das partes distintas em um único componente, otimizado para uma estrutura ou pilha específica, que cuida de todo o trabalho de cola para você. Se você criar algo assim, avise-nos para que possamos ajudar a divulgar o material.

Por fim, além das consultas de contêiner, existem tantos recursos incríveis de CSS e interface que agora são interoperáveis com todos os principais mecanismos de navegador. Como comunidade, vamos descobrir como usar esses recursos para beneficiar nossos usuários.