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
- 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
Seu navegador ainda não suporta flex-wrap: balance (disponível a partir do Chrome 150), então
esta demonstração mostra a quebra de linha comum.
- Início
- Sobre
- Contato
- Trabalhos
- Serviços
- Palestras
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:
À 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.
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
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
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
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
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
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
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
Seu navegador ainda não suporta flex-wrap: balance (disponível a partir do Chrome 150), então
esta demonstração mostra a quebra de linha comum.
- 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
Ahmad Café
- 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
Ahmad Café
- 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
Seu navegador ainda não suporta flex-wrap: balance nem flex-line-count (disponíveis a
partir do Chrome 150), então a navegação quebra linha do jeito comum.
Ahmad Café
- 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
Seu navegador ainda não suporta flex-line-count (disponível a partir do Chrome 150), então os cards
caem num grid comum.
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
Seu navegador ainda não suporta flex-line-count (disponível a partir do Chrome 150), então os cards
caem num grid comum.
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.
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.