Por que não Flexbox?

Quando a gente pensa em um layout zigzag, a primeira ideia é usar flex-direction: column com flex-wrap: wrap. Parece natural — os itens descem e depois quebram para a próxima coluna.

Mas esse approach tem dois problemas sérios:

  • Altura fixa no container. O flex container precisa saber exatamente a altura para o wrapping funcionar. Muda o conteúdo, quebra o layout.
  • Ordem de tabulação bagunçada. Os itens descem na primeira coluna (1, 2, 3) e pulam para a segunda (4, 5, 6). Visualmente é um zigzag, mas o foco do teclado segue o DOM — um caminho completamente diferente.

O CSS Grid resolve os dois problemas de forma elegante. O trade-off? Você vai precisar definir um valor fixo: a altura do item. Mas o resto do layout fica flexível. Vamos construir!

Two-column zigzag CSS grid layout with staggered items on a laptop screen Software Concept Art

A Base com Grid

Comece com um grid de duas colunas. Sem truques ainda.

*, *::before, *::after {
  box-sizing: border-box;
}

.wrapper {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  max-width: 800px;
  margin: 0 auto;
}

.item {
  height: 100px;
  border: 2px solid;
}

Por que box-sizing: border-box? Sem ele, um item de 100px com borda de 2px na verdade tem 104px. Essa diferença de 4px vai bagunçar a matemática da transformação.

O Stagger: translateY(50%)

Aqui está o truque principal — selecione todos os itens pares e desloque-os para baixo pela metade da própria altura:

/* Seleciona itens pares pela classe, não pelo nome da tag */
.item:nth-child(even of .item) {
  transform: translateY(50%);
}

Por que :nth-child(even of .item) em vez de .item:nth-of-type(even)? O segundo seleciona pelo nome da tag. Misture um <div> e um <span> no mesmo container, e o nth-of-type vai contar separadamente. A sintaxe of .item filtra pela classe explicitamente — mais precisa e com suporte nos browsers modernos.

A Mágica das Porcentagens no Transform

Aqui está o que faz isso funcionar: em transforms CSS, porcentagens se referem ao próprio elemento, não ao pai.

  • width: 50% → 50% da largura do pai.
  • translateY(50%) → 50% da altura do próprio elemento.

É por isso que o stagger fica proporcional. Se seus itens têm 200px, eles deslocam 100px. Se têm 50px, deslocam 25px. O layout se adapta automaticamente.

A Correção do Gap

Defina o gap para um valor grande (tipo 100px) e você vai ver o problema: os itens pares não deslocam o suficiente. Precisam considerar o espaço vertical entre as linhas.

Guarde o gap como uma propriedade customizada e adicione à tradução:

.wrapper {
  --gap: 16px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  max-width: 800px;
  margin: 0 auto;
}

.item:nth-child(even of .item) {
  transform: translateY(calc(50% + var(--gap) / 2));
}

Dividimos o gap por 2 porque precisamos só da metade da distância entre as linhas. O valor inteiro passaria do ponto.

O Problema do Overflow

Adicione uma borda vermelha no wrapper e coloque um sexto item. Ele vaza. Por quê?

Transforms não afetam o layout. O navegador posiciona tudo primeiro, depois desloca os pixels visualmente. O wrapper se dimensiona com base nas posições originais, não nas transformadas.

Correção: Reserve Espaço com Padding

Armazene a altura do item como propriedade customizada e adicione padding equivalente no wrapper:

.wrapper {
  --gap: 16px;
  --item-height: 100px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  margin: 0 auto;
  max-width: 800px;
  padding-bottom: calc(var(--item-height) / 2 + var(--gap) / 2);
}

.item {
  border: 2px solid;
  height: var(--item-height);
}

Sim, --item-height: 100px é um valor fixo — a mesma fragilidade que criticamos no flexbox. Mas aqui você está definindo a altura do item, não a do container. A estrutura de colunas, o gap e a ordem de origem continuam flexíveis. É um trade-off, não um impedimento.

Developer coding a zigzag grid layout with CSS transforms in a code editor IT Technology Image

Considerações de Acessibilidade

  • Leitores de tela não são afetados. Transforms são puramente visuais. A ordem do DOM permanece 1-2-3-4-5-6, e é exatamente assim que a tecnologia assistiva anuncia.
  • Ordem de foco permanece intacta. A navegação por teclado segue a ordem de origem. No nosso zigzag, a ordem visual e a de origem coincidem (esquerda-direita, cima-baixo), então não há conflito.
  • Respeite preferências de movimento. Se você animar o stagger no carregamento da página, envolva em um prefers-reduced-motion:
@media (prefers-reduced-motion: no-preference) {
  .item {
    animation: slide-in 0.3s ease-out both;
  }
}

Limitações e Cuidados

  • Altura fixa do item. Se seus itens têm conteúdo variável, essa abordagem não funciona sem JavaScript para medir cada um.
  • Sem redimensionamento dinâmico. Se os itens mudarem de altura após o carregamento (ex: accordions), o stagger quebra.
  • Suporte de browsers: :nth-child(even of .item) é suportado em todos os browsers modernos (Chrome 111+, Firefox 113+, Safari 15.4+). Para browsers antigos, use um fallback mais simples.

Próximos Passos

  • Explore subgrid para alinhar itens zigzag aninhados.
  • Teste container queries para ajustar a altura do item com base na largura do container.
  • Combine com aspect-ratio para itens responsivos que mantêm proporções.

Conclusão

O layout zigzag são três ideias empilhadas:

  1. Grid de duas colunas como fundação.
  2. translateY(50%) cria o stagger porque porcentagens em transforms se referem ao próprio elemento.
  3. padding-bottom reserva espaço para os itens transformados — transforms movem pixels sem avisar o engine de layout.

Mude o gap. Mude a altura do item. Adicione mais itens. O zigzag se mantém.


Referência: artigo original no CSS-Tricks

Leitura Relacionada

Minimal desk setup with code editor showing CSS grid gap and transform calculation Dev Environment Setup

Este conteúdo foi elaborado com o auxílio de ferramentas de IA, com base em fontes confiáveis, e revisado pela nossa equipe editorial antes da publicação. Não substitui o aconselhamento de um profissional especializado.