Create a Dynamic Zigzag Layout with CSS Grid and TranslateY
The Challenge of Non-Linear Layouts
Most grid-based designs rely on straight rows and columns, creating a structured, predictable rhythm. However, sometimes you need a layout that feels more organic—like items cascading diagonally in a waterfall pattern. This zigzag effect adds visual interest and can help break the monotony of standard designs.

Building such a layout might seem tricky at first, but with a clever combination of CSS Grid and the translateY() transform, you can achieve a smooth, staggered arrangement without breaking accessibility or logical order. Let's explore the strategy and implementation step by step.
Initial Flexbox Approach and Its Pitfalls
One might first consider using Flexbox with flex-direction: column and flex-wrap: wrap. This would allow items to flow down a column and then wrap into a second column. While conceptually simple, this method introduces two significant problems:
- Fixed height requirement: You must define a fixed height for the container (e.g., 500px) to make wrapping work. This makes the layout brittle and unresponsive.
- Broken tab order: Items flow down the first column (1, 2, 3) then jump to the second (4, 5, 6). This disrupts the natural reading and keyboard navigation order—hardly a waterfall effect.
The CSS Grid approach, which we'll build next, avoids the tab order issue entirely and only requires a single hardcoded value (the translation amount), which is far more manageable.
The Grid Strategy
The core idea is straightforward:
- Create a two-column grid with items placed side by side.
- Select every item in the second column (the even-numbered ones).
- Shift them down by half of their own height to create the staggered zigzag.
This translation is where the magic happens. Let's implement it.
Setting Up the Grid
Start with a wrapper containing five items. The markup is simple:
<div class="wrapper">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>Now apply the basic CSS:
*,
*::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;
}Notice the global box-sizing: border-box. Without it, the border adds extra height, making items taller than 100px. That would break the exact 50% translation we need later.
Applying the Vertical Shift
Now select every even item (the second column) and translate it downward by half its height:
.item:nth-child(even of .item) {
transform: translateY(50%);
}Why use :nth-child(even of .item) instead of the more common :nth-of-type(even)? In this demo all children are the same element type, so both would work. But :nth-of-type selects by tag name, not by class. If you ever mix element types (e.g., a heading inside the wrapper), it will match unexpectedly. The of .item syntax is more precise—it only considers elements that match the class .item.
The result is a perfect zigzag: items in the first column stay at their natural top position, while those in the second column drop by 50% of their own height, creating a cascading effect.
Why This Works
The translateY(50%) operates relative to the element's own height. Because all items share the same height (thanks to the fixed 100px value and box-sizing), the shift is consistent. Grid placement already puts even items in the second column automatically, so no extra positioning is needed.
This method preserves the natural tab order: items are read left-to-right, top-to-bottom (1,2,3,4,5) because the DOM order remains unchanged. The visual displacement does not affect the logical flow—a key advantage over the Flexbox wrapping approach.
Considerations and Alternatives
Responsive Adaptations
If you switch to a single column on small screens, disable the transform and adjust the grid to one column:
@media (max-width: 600px) {
.wrapper {
grid-template-columns: 1fr;
}
.item:nth-child(even of .item) {
transform: none;
}
}Varying Item Heights
If your items have different heights, the 50% shift will create uneven drops. You can still achieve a zigzag, but the pattern becomes less predictable. In that case, consider using a fixed translation value (e.g., translateY(30px)) or JavaScript to calculate heights dynamically.
Browser Support
The :nth-child( even of .item ) syntax is supported in all modern browsers (Chrome, Firefox, Safari, Edge). For older browsers, fall back to .item:nth-of-type(even) if the element types are consistent.
Conclusion
By combining CSS Grid with a well-placed transform, you can create an elegant zigzag layout that maintains proper DOM order and responsiveness. The trick lies in understanding how translateY(50%) references the element's own height, and how :nth-child with a class filter provides precise selection. This technique is a small but powerful addition to any front-end developer’s toolkit—perfect for adding rhythm to otherwise static designs.
Related Articles
- Choosing Between CommonJS and ESM: A Practical Guide to JavaScript Module Architecture
- Web Development Breakthroughs: HTML in Canvas, Hex Map Analytics, E-Ink OS, and CSS Image Replacement
- Achieving Major JSON.stringify Performance Gains: A Deep Dive into V8's Optimizations
- Mastering Modern Web Experiments: HTML in Canvas, Hex Maps, E-ink Tweaks, and CSS Image Sorcery
- Reviving the Dream of a Machine-Readable Web: The Case for Simplified Structured Data
- Mastering Business Days Calculation in JavaScript: A Practical Q&A
- 7 Essential Steps for Browser-Based Vue Testing Without Node
- Exploring the Latest Web Innovations: Canvas HTML, Hexagonal Analytics, E-Ink OS, and CSS Image Swaps