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!

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.

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
subgridpara alinhar itens zigzag aninhados. - Teste
container queriespara ajustar a altura do item com base na largura do container. - Combine com
aspect-ratiopara itens responsivos que mantêm proporções.
Conclusão
O layout zigzag são três ideias empilhadas:
- Grid de duas colunas como fundação.
translateY(50%)cria o stagger porque porcentagens em transforms se referem ao próprio elemento.padding-bottomreserva 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
- Beyond the Model: Como a Pantone Construiu uma Base de Dados Pronta para IA
- Vercel Workflow Ficou 2x Mais Rápido — O Que Mudou e Como Usar
