Novo Livro!
Categorias
Projetos
CSS

Equilibrando itens flex com flex-wrap: balance

Equilibrando itens flex com flex-wrap: balance

Quando itens flex quebram linha, é comum um deles sobrar sozinho na última linha. O flex-wrap: balance resolve isso distribuindo os itens por igual entre as linhas, sem media query nenhuma.

Aqui estão o problema em demonstrações interativas, quatro formas de contorná-lo enquanto o recurso só existe no Chrome e o flex-line-count, que define um número mínimo de linhas e permite até um layout masonry.

Introdução

Ao trabalhar com flexbox e quebra de linha ativada, é comum chegar a uma situação em que os itens quebram linha, mas, em algum momento, só um deles vai para a linha de baixo: um item órfão.

Muitas vezes, quem desenha ou desenvolve a interface recorre a uma media query para evitar esse comportamento, o que está longe do ideal e quebra fácil se o conteúdo mudar.

Um exemplo mostra o problema.

.list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
}

Esta é a demonstração. Todas as demonstrações deste artigo mudam de largura pelo controle deslizante ou arrastando o canto inferior direito do quadro tracejado:

Demonstração: lista com flex-wrap: wrap e um item órfão

540px
  • Início
  • Sobre
  • Contato
  • Trabalhos
  • Serviços
  • Palestras

O último item foi sozinho para uma nova linha. Não parece certo.

flex-wrap: balance

Felizmente, agora existe o flex-wrap: balance, disponível a partir do Chrome 150. Com ele, o resultado fica muito mais equilibrado.

Ligue e desligue o flex-wrap: balance para ver como ele funciona bem (só no Chrome).

Demonstração: liga e desliga flex-wrap: balance na mesma lista

540px
  • Início
  • Sobre
  • Contato
  • Trabalhos
  • Serviços
  • Palestras
Gravação da demonstração acima no Chrome, para quem lê num navegador sem suporte a flex-wrap: balance.

Antes de usar o flex-wrap: balance, vale resolver um exemplo comum sem ele, já que o recurso ainda só existe no Chrome.

Exemplo real: a seção de redes sociais da Buffer.com

Essa seção tem dois elementos principais:

  • O título
  • Uma lista de ícones de redes sociais

Veja:

Seção da Buffer com o título Connect your favorite accounts à esquerda e uma fileira de ícones de redes sociais à direita.

À primeira vista, parece tudo certo. Mas, ao redimensionar a viewport, o layout começa a quebrar. Veja o vídeo abaixo:

O wrapper dos ícones é um flex container com justify-content: space-between. É isso que faz os ícones quebrarem linha e deixa dois ícones numa mesma linha, longe um do outro.

Dificilmente isso é intencional.

A mesma seção da Buffer em cinco larguras: conforme ela estreita, os ícones que sobram ficam espalhados na segunda linha, distantes uns dos outros.

Hora de corrigir. A seção foi replicada nas demonstrações abaixo para testar como resolver esse problema de layout.

Para caber na coluna de texto, nas réplicas o título fica sempre acima dos ícones. Abaixo de cada uma aparece a largura do contêiner dos canais, que é a medida que as container queries comparam com os valores do CSS.

Solução 1: container queries

O primeiro passo é deixar o flex container quebrar linha normalmente e usar justify-content: center para centralizar os ícones.

.channels {
display: flex;
flex-wrap: wrap;
justify-content: center;
}

Demonstração: ícones centralizados com flex-wrap: wrap

480px

Conecte suas contas favoritas

  • LinkedIn
  • Threads
  • Pinterest
  • Bluesky
  • YouTube
  • X
  • Google Business
  • Instagram

Largura do contêiner dos canais:

A ideia é esta:

  • Definir um contêiner para os canais.
  • Calcular a largura de cada canal mais o espaço entre eles.
  • Se a largura do contêiner for menor ou igual à largura calculada dos canais, o layout é forçado a quebrar linha.

Essa solução funciona em qualquer navegador com suporte a container queries. A ideia é definir um max-width nos contêineres pequenos para mostrar um número predefinido de canais por linha, aqui 5.

.section-wrapper {
container-type: inline-size;
container-name: demo;
}
.section {
--size: 44px;
--gap: 1rem;
--count: 8;
--width: calc(var(--size) * var(--count) + var(--gap) * (var(--count) - 1));
--items-row: 5;
--max-width: calc(
var(--size) * var(--items-row) + var(--gap) * (var(--items-row) - 1)
);
}
.channels {
max-width: var(--max-width);
/* --width é 464px. Não dá para usar variável CSS dentro de uma media query. */
@container (width >= 464px) {
max-width: initial;
background-color: rgba(from deeppink r g b / 0.3);
}
}

Mexa no controle de largura, ou arraste o canto do contêiner, e veja como o layout muda. O fundo rosa aparece quando a container query passa a valer, ou seja, quando o contêiner dos canais chega a 464px.

Demonstração: solução 1, max-width com container query

480px

Conecte suas contas favoritas

  • LinkedIn
  • Threads
  • Pinterest
  • Bluesky
  • YouTube
  • X
  • Google Business
  • Instagram

Largura do contêiner dos canais:

Solução 2: max-width com um clamp condicional

Nesta solução, o elemento pai vira um contêiner e o max-width cuida do layout.

.section-wrapper {
container-type: inline-size;
container-name: demo;
}
.section {
--size: 44px;
--gap: 1rem;
--count: 8;
--items-row: 5;
--width: calc(var(--size) * var(--count) + var(--gap) * (var(--count) - 1));
--min: calc(
var(--size) * var(--items-row) + var(--gap) * (var(--items-row) - 1)
);
--max-width: clamp(var(--min), (100cqw - var(--width)) * 999, 100%);
.channels {
max-width: var(--max-width);
}
}

É preciso saber de antemão:

  • O tamanho de cada canal (44px)
  • O espaço entre eles (gap)
  • A quantidade total
  • O número de itens por linha

A partir disso, dá para calcular o número mínimo de itens por linha e a largura máxima.

Esta é a demonstração funcionando:

Demonstração: solução 2, max-width com clamp condicional

480px

Conecte suas contas favoritas

  • LinkedIn
  • Threads
  • Pinterest
  • Bluesky
  • YouTube
  • X
  • Google Business
  • Instagram

Largura do contêiner dos canais:

Para quem ficou curioso, essa solução usa um clamp condicional, assunto de um artigo anterior.

Solução 3: display contents

Nesta solução, os itens são divididos em dois grupos. Em tamanhos menores, os grupos são achatados com display: contents. Em tamanhos maiores, os grupos aparecem para imitar a quebra equilibrada.

<div class="channels">
<div class="group-1">
<!-- primeira metade dos canais -->
</div>
<div class="group-2">
<!-- segunda metade dos canais -->
</div>
</div>
.channels {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
> div {
/* Ignora os grupos em tamanhos pequenos. */
display: contents;
@container (width >= 500px) {
display: inherit;
flex-wrap: inherit;
justify-content: inherit;
gap: inherit;
background-color: rgba(from deeppink r g b / 0.3);
}
}
}

Demonstração: solução 3, dois grupos com display: contents

Conecte suas contas favoritas

LinkedIn
Threads
Pinterest
Bluesky
YouTube
X
Google Business
Instagram
Mastodon
TikTok

Largura do contêiner dos canais:

Funciona, mas não é a solução mais elegante. Ainda assim, é bem melhor do que usar só justify-content: space-between.

Para saber mais, há um artigo sobre display: contents.

Solução 4: um item flex no meio

Esta solução não funcionou bem, mas vale mostrar mesmo assim. A ideia é adicionar um pseudo-elemento e usar a propriedade order.

O pseudo-elemento é forçado a ocupar a linha inteira e vai para o meio da lista com a ajuda da propriedade --count.

ul {
&:before {
content: "";
order: calc(var(--count) / 2 + 1);
flex: 0 0 100%;
}
}
li {
&:nth-child(1) {
order: 1;
}
/* e assim por diante... */
}

Na demonstração, o pseudo-elemento aparece como uma linha rosa. Estreite o contêiner até caberem só três ícones por linha para ver onde a solução falha: a quebra no meio continua lá, e os órfãos voltam.

Demonstração: solução 4, pseudo-elemento que quebra a lista no meio

480px

Conecte suas contas favoritas

  • LinkedIn
  • Threads
  • Pinterest
  • Bluesky
  • YouTube
  • X
  • Google Business
  • Instagram

Largura do contêiner dos canais:

Solução 5: flex-wrap balance

Depois de todas essas soluções, veja como é simples usar o flex-wrap: balance.

.channels {
display: flex;
flex-wrap: balance;
justify-content: center;
gap: 1rem;
}

Demonstração: solução 5, flex-wrap: balance

480px

Conecte suas contas favoritas

  • LinkedIn
  • Threads
  • Pinterest
  • Bluesky
  • YouTube
  • X
  • Google Business
  • Instagram

Largura do contêiner dos canais:

Só isso!

flex-line-count

A pesquisa sobre flex-wrap: balance revelou outra propriedade CSS nova: flex-line-count.

Ela define um número mínimo de linhas para os itens flex que quebram. Mesmo que haja espaço de sobra e os itens não precisem quebrar, dá para forçar um número mínimo de linhas.

.list {
display: flex;
flex-wrap: balance;
flex-line-count: 2;
gap: 0.5rem;
}

Aqui, a quebra começa com pelo menos duas linhas, mesmo com espaço suficiente para os itens ficarem numa só.

Demonstração: flex-line-count define o mínimo de linhas

  • Início
  • Sobre
  • Contato
  • Trabalhos
  • Serviços
  • Palestras

Parece mágica, e deve abrir um mundo de casos de uso que antes não eram possíveis. Como tirar proveito disso?

Demonstração de navegação

Neste exemplo, há um cabeçalho em que a navegação deve sempre ocupar duas linhas. Normalmente, a navegação seria limitada assim:

  • Definindo uma largura máxima.
  • Definindo um grid com largura fixa para a navegação.
  • E outras formas…

A demonstração a seguir usa a clássica solução com max-width, marcada pelo contorno tracejado. Veja:

.nav ul {
max-width: 320px;
margin-inline: auto;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
}

Demonstração: navegação limitada por max-width

  • Espresso
  • Coado
  • Cold brew
  • Doces
  • Café da manhã
  • Grãos
  • Sazonais

Funciona, mas perde-se a liberdade de estilizar o wrapper da navegação. Se for preciso um fundo colorido, por exemplo, ele fica restrito ao max-width.

Demonstração: o fundo da navegação fica preso ao max-width

  • Espresso
  • Coado
  • Cold brew
  • Doces
  • Café da manhã
  • Grãos
  • Sazonais

Com o flex-line-count, dá para definir o número mínimo de linhas dos itens que quebram, e a largura do wrapper da navegação continua a mesma.

.nav ul {
display: flex;
justify-content: center;
flex-wrap: balance;
flex-line-count: 2;
gap: 0.5rem;
}

Demonstração: navegação em duas linhas com flex-line-count

  • Espresso
  • Coado
  • Cold brew
  • Doces
  • Café da manhã
  • Grãos
  • Sazonais

Layout masonry com flex-line-count

Dá para fazer um layout masonry com flex-line-count? A resposta curta é sim. Veja como.

Primeiro, um exemplo simples para testar a ideia; depois, um com conteúdo de verdade.

.layout {
display: flex;
flex-direction: column;
flex-wrap: balance;
flex-line-count: 4;
gap: 1.5rem;
}

Demonstração: masonry de teste com flex-line-count

Curto, intenso e servido em xícara pequena.

Uma chaleira, um filtro e um pouco de paciência.

Pó de moagem grossa em água fria durante a noite, depois coado. O resultado é suave, pouco ácido e dura alguns dias na geladeira.

O clássico do fogão, pronto antes de a torrada pular.

Moagem grossa, quatro minutos e depois é só pressionar. Encorpado e com um pouco de borra, o que é qualidade ou defeito dependendo de quem responde.

Pequena, leve e difícil de errar.

Partes iguais de espresso e leite vaporizado.

Café moído bem fino, água e açúcar vão juntos para o cezve. Servido sem coar, com a borra assentando no fundo enquanto se bebe.

À primeira vista, está bom. Mas, ao redimensionar, o número de colunas continua o mesmo, e os itens ficam pequenos demais.

Como resolver? O plano é este:

  • Transformar o layout em contêiner.
  • Definir a largura do card.
  • Dividir a largura do contêiner pela largura do card para obter o número de colunas.
  • Usar a função round() para chegar ao inteiro mais próximo.
.layout {
--line-min: 190px;
--line-count: clamp(1, round(down, 100cqw / var(--line-min)), 4);
display: flex;
flex-direction: column;
flex-wrap: balance;
flex-line-count: var(--line-count);
gap: 1.5rem;
}

Veja a demonstração abaixo:

Demonstração: masonry de cards com o número de colunas calculado pela largura

Espresso

Curto, intenso e servido em xícara pequena.

Como passar um café coado que tenha mesmo as notas de origem do pacote

Uma chaleira, um filtro e um pouco de paciência.

Cold brew

Pó de moagem grossa em água fria durante a noite, depois coado. O resultado é suave, pouco ácido e dura alguns dias na geladeira.

Moka numa manhã de dia útil

O clássico do fogão, pronto antes de a torrada pular.

Prensa francesa

Moagem grossa, quatro minutos e depois é só pressionar. Encorpado e com um pouco de borra, o que é qualidade ou defeito dependendo de quem responde.

AeroPress, caneca de viagem, chaleira de hotel: a rotina inteira numa bolsa

Pequena, leve e difícil de errar.

Cortado

Partes iguais de espresso e leite vaporizado.

Café turco, fervido com o pó deixado na xícara

Café moído bem fino, água e açúcar vão juntos para o cezve. Servido sem coar, com a borra assentando no fundo enquanto se bebe.

Gravação da demonstração acima no Chrome, para quem lê num navegador sem suporte a flex-line-count.

Esta demonstração é só um experimento, e vale lembrar que a ordem de leitura não é a mesma da ordem visual (os números nos cards mostram a ordem no HTML). Ainda assim, ela leva o flex-line-count ao máximo do que ele consegue fazer, ou quase.

Para outra abordagem de masonry que está a caminho do CSS, veja Item Flow, o novo sistema de layout para CSS.

Conclusão

O flex-wrap: balance é um ótimo recurso novo, que vale usar cada vez mais. A surpresa foi o flex-line-count, bem mais útil do que parecia à primeira vista. Agora fica a curiosidade de ver o que a comunidade vai criar com os dois.