Creating Dynamic Zigzag Layouts with CSS Grid and Transform

By ⚡ min read

Introduction

Most grid layouts sit in neat rows, perfectly aligned, like soldiers in formation. But sometimes you want something with more rhythm — a layout where items cascade diagonally, like water flowing down a waterfall. This is the zigzag layout. And building it requires a clever trick that reveals a fascinating detail about how CSS transforms actually work.

Creating Dynamic Zigzag Layouts with CSS Grid and Transform
Source: css-tricks.com

The Strategy

Before writing a single line of CSS, let’s think about approach. The first idea that might pop into your head is using a flex container with flex-direction: column and flex-wrap: wrap. This would make items flow down and then wrap into a second column. Flexbox is flexible enough to work in either orientation, so it seems like a natural choice.

However, two problems make this approach awkward:

  • Fixed height required: You have to tell the container “you are 500px tall” for wrapping to kick in. That’s brittle and hard to maintain.
  • Broken tab order: Items flow down the first column (1, 2, 3) then jump to the second column (4, 5, 6). That’s not a waterfall; it’s two buckets. Keyboard navigation suffers, disrupting accessibility.

To be fair, the CSS Grid approach we’re about to build has its own hardcoded value — we’ll get to that. But it sidesteps the tab order problem entirely, and that’s a meaningful win.

The Grid Plan

Here’s what I want to do instead:

  1. Create a two-column grid with items sitting side by side, nothing fancy.
  2. Select every item in the second column — the even ones.
  3. Shift them down by half of their own height to establish the staggered layout.

That shift is where the magic happens. Let’s build it step by step.

The Grid Setup

We start with a wrapper and five items. Nothing in the file yet, just a blank slate.

<div class="wrapper">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

*,
*::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;
}

We’re applying box-sizing: border-box globally because without it, the items aren’t actually 100px tall — they’re slightly taller once the border gets added. This will matter in a moment.

The Shift

Now the fun part. Let’s grab every even item and translate it down:

.item:nth-child(even of .item) {
  transform: translateY(50%);
}

A quick note on the selector. You might reach for .item:nth-of-type(even) here, and in this demo it would produce the same result since all the children are the same element type. But nth-of-type selects by tag name, not by class. So if you ever mix different element types inside the wrapper, it’ll match in ways you don’t expect. :nth-child(even of .item) is more precise because it limits the count to only those elements with the .item class. This keeps your layout robust.

The translateY(50%) value is half the element’s own height. Since the item is 100px tall (thanks to box-sizing), it shifts down 50px. This creates the staggered, zigzag effect.

Why Transform Works So Well

Using transform instead of margins or positioning has a hidden benefit: it doesn’t affect the document flow. The grid still thinks each item is in its original position, so the columns stay aligned and the gaps remain consistent. Meanwhile, the visual layer shifts items down, creating the illusion of a waterfall.

This technique is especially useful when you want the layout to remain responsive. If the items have different heights (e.g., due to varying content), you can use translateY(50%) and it will still shift each item by half its own height, automatically adapting. For a fixed-height layout like our demo, the result is predictable and clean.

Making It Responsive

One concern is that the grid currently has two fixed columns. On smaller screens, you might want to collapse to a single column. That’s easy with a media query:

@media (max-width: 600px) {
  .wrapper {
    grid-template-columns: 1fr;
  }
  .item:nth-child(even of .item) {
    transform: none; /* remove the shift */
  }
}

Now on mobile, items stack vertically with no zigzag. You could even add a small margin instead if you want a subtle offset.

Additional Tweaks

You can customize the zigzag further:

  • Change shift amount: Replace 50% with 25% or 75% for different stagger depths.
  • Apply to odd items: Use nth-child(odd of .item) to shift the first column instead.
  • Add rotation: Combine translateY with rotate(2deg) for a playful twist.
  • Animation: Use CSS transitions or keyframes to animate the shift on hover or scroll.

Conclusion

The zigzag layout is a perfect example of how a small CSS trick — in this case, transform: translateY(50%) on even items — can turn a mundane grid into a dynamic, engaging design. By avoiding flexbox’s tab order issues and relying on pure CSS Grid with a clever selector, we get a layout that’s both accessible and visually appealing. Next time you need a layout with rhythm, give this technique a try.

Recommended

Discover More

LNP Transmission Plan Under Fire: Experts Warn of Blackout Risks and Political GamesBecoming a Member of the Python Security Response Team: A Step-by-Step GuideFarm-Led 400MW Battery Clears Federal Environmental Hurdle in Record 30 DaysVolla Phone Plinius: A Rugged Mid-Range Smartphone with Dual OS OptionsRobinhood’s Venture Fund Attracts Over 150,000 Retail Investors Ahead of IPO, CEO Confirms