Most grid layouts sit in neat rows, completely aligned, like troopers in formation. However generally you need one thing with extra rhythm — a format the place gadgets cascade diagonally, like water flowing down a waterfall.
That is the zigzag format. And constructing it requires a small trick that reveals one thing fascinating about how CSS transforms truly work.
The Technique
Earlier than writing a single line of CSS, let’s take into consideration strategy.
The primary concept that involves thoughts: arrange a flex container with flex-direction: column and flex-wrap: wrap, so gadgets movement down after which wrap right into a second column. Often we consider the flex-wrap property when it comes to rows, however the good factor about flexbox is that it really works in both orientation.
Two issues make this strategy awkward:
- You want a set top. It’s important to inform the container “you might be
500pxtall” for wrapping to kick in. That’s brittle. - The tab order breaks. Objects movement down the primary column (i.e., 1, 2, 3), then leap to the second column (i.e., 4, 5, 6). That’s not a waterfall. That’s two buckets.
To be truthful, the CSS Grid strategy we’re about to construct has its personal hardcoded worth. We’ll get to that. But it surely sidesteps the Tab order downside fully, and that’s a significant win.
The Grid Plan
Right here’s what I wish to do as a substitute:
- Create a two-column grid with gadgets sitting facet by facet, nothing fancy.
- Choose each merchandise within the second column, the even ones.
- Shift them down by half of their very own top to ascertain the staggered format.
That shift is the place the magic occurs. Let’s construct it.
The Grid
We begin with a wrapper and 5 gadgets. Nothing within the file but, only a clean slate.
*,
*::earlier than,
*::after {
box-sizing: border-box;
}
.wrapper {
show: grid;
grid-template-columns: 1fr 1fr;
hole: 16px;
max-width: 800px;
margin: 0 auto;
}
.merchandise {
top: 100px;
border: 2px stable;
}
We’re making use of box-sizing: border-box globally as a result of with out it, the gadgets aren’t truly 100px tall — they’re barely taller as soon as the border will get added. It will matter in a second.
The Shift
Now the enjoyable half. Let’s seize each even merchandise and translate it down:
.merchandise:nth-child(even of .merchandise) {
rework: translateY(50%);
}
A fast be aware on the selector. You would possibly attain for .merchandise:nth-of-type(even) right here, and on this demo it will produce the identical outcome since all the youngsters are the identical factor kind. However nth-of-type selects by tag identify, not by class. So in case you ever combine totally different factor varieties contained in the wrapper, it’ll match in methods you don’t count on. :nth-child(even of .merchandise) is extra exact as a result of it explicitly filters by class, and it’s well-supported in trendy browsers.
The zigzag emerges instantly. However let’s pause right here, as a result of one thing delicate is going on and it’s price understanding.
Remodel Percentages Are Completely different
Percentages in transforms work fully otherwise than they do anyplace else in CSS.
In movement format, positioned format, or actually any format mode, a share refers back to the father or mother’s out there house. For those who write width: 50% on a component inside a wrapper, you’re saying: The container is that this large. Make me half of that.
Transforms don’t work this manner. In a rework, percentages check with the factor itself. So translateY(50%) doesn’t imply “transfer down by half of the out there house.” It means “transfer down by half of your individual top.” If the factor is 200px tall, it strikes down by 100px.
That is truly the identical coordinate-system habits you see with the person translate(), scale(), and rotate() CSS properties. All of them are utilized within the factor’s personal coordinate house, post-layout. The browser finishes laying the whole lot out first, together with positions, sizes — mainly the entire field mannequin — after which applies the rework relative to the factor itself. That’s why scale(2) grows outward from the factor’s middle, not from the top-left of the web page.
That is precisely why the trick works. Every even merchandise shifts down relative to its personal measurement, not the container’s. The zigzag stays proportional irrespective of how tall the gadgets are.
The outcome seems to be shut. But it surely’s not fairly proper.
The Hole Drawback
We are able to expose the imperfection by cranking the hole as much as one thing absurd — say, 100px. Once we do, the even gadgets clearly aren’t sitting the place they need to. They should journey somewhat additional to account for the vertical house between rows.
Right here’s the repair. First, let’s retailer the hole in a CSS customized property so we are able to reference it in a number of locations:
.wrapper {
--gap: 16px;
show: grid;
grid-template-columns: 1fr 1fr;
hole: var(--gap);
max-width: 800px;
margin: 0 auto;
}
.merchandise:nth-child(even of .merchandise) {
rework: translateY(calc(50% + var(--gap) / 2));
}
We translate by 50% of the factor’s top plus half of the hole. We divide the hole by 2 as a result of we solely have to cowl half the gap between rows — the complete worth would push it too far.
Set the hole to 16px, it seems to be nice. Set it to 100px, it nonetheless seems to be nice. The mathematics holds whatever the worth.
The Overflow Shock
We’ve solved the core puzzle. However there’s a hidden downside ready to floor.
Let’s add a border to the wrapper to see its boundaries:
.wrapper {
border: 2px stable crimson;
}
With 5 gadgets, the whole lot seems to be high-quality. The wrapper incorporates all of its youngsters. No overflow. No points.
Now add a sixth merchandise:
The sixth merchandise is even. It will get translated down. And it spills proper out of the container.
Why? As a result of transforms don’t have an effect on format. So far as the browser’s format engine is worried, that sixth merchandise continues to be sitting in its authentic, untranslated place. The wrapper sizes itself primarily based on that authentic place. The rework shifts pixels visually, however the father or mother has no concept something moved.
We stunned the browser.
The Repair: Reserve the House
The best answer is so as to add padding-bottom (or padding-block-end) to the wrapper, sufficient to accommodate the overshoot. The padding must match the interpretation: half the merchandise top plus half the hole.
Since padding percentages reference the father or mother’s width (not the kid’s top), we are able to’t use the identical 50% trick right here. As an alternative, we retailer the merchandise top as a variable:
.wrapper {
--gap: 16px;
--item-height: 100px;
show: grid;
grid-template-columns: 1fr 1fr;
hole: var(--gap);
margin: 0 auto;
max-width: 800px;
padding-bottom: calc(var(--item-height) / 2 + var(--gap) / 2);
}
.merchandise {
border: 2px stable;
top: var(--item-height);
}
Now, I’ll be up entrance: --item-height: 100px is a hard-coded worth. That’s the identical sort of brittleness I flagged within the flexbox strategy, the place you want a set container top for wrapping to work. Each approaches ask you to know a dimension forward of time. The distinction right here is that you just’re locking down the merchandise top somewhat than the container top, and the remainder of the format — column construction, hole math, supply order — stays versatile. It’s a trade-off, not a deal-breaker, nevertheless it’s price being sincere about.
The wrapper now reserves precisely sufficient house on the backside. No overflow. No surprises.
A Observe on Accessibility
This strategy retains gadgets of their pure supply order, and that issues greater than it may appear at first look.
Display screen readers are unaffected. Transforms are purely visible. The DOM order stays 1-6, and that’s precisely how assistive expertise will announce them. No reordering surprises, in contrast to the flexbox column-wrap strategy the place the visible order and DOM order can diverge.
Focus order stays intact, too. When somebody tabs by the gadgets, focus follows the supply order, not the place the gadgets seem visually. In our zigzag, the visible movement and supply order each cascade left-right, top-down, in order that they naturally agree. In case your format ever will get complicated sufficient that visible and supply order begin to diverge, that’s once you’d have to suppose extra fastidiously about focus administration.
Respect movement preferences. The zigzag itself is static — we’re not animating the rework. However in case you ever resolve to animate gadgets into their staggered positions (say, on web page load), wrap that animation in a prefers-reduced-motion verify:
/* animates when consumer has no movement choice */
@media (prefers-reduced-motion: no-preference) {
.merchandise {
animation: slide-in 0.3s ease-out each;
}
}
On this case, we’ve set it up in order that customers who haven't any choice on movement are the one ones who get the animation. Usually, although, you would possibly do the inverse of that. The format nonetheless works both method.
The Last Demo
As soon as once more:
Conclusion
The zigzag format is absolutely simply three concepts stacked on prime of one another:
- A two-column grid offers us the inspiration.
translateY(50%)creates the stagger and works as a result of rework percentages reference the factor itself, not the father or mother.padding-bottomreserves house for the translated gadgets as a result of transforms transfer pixels with out telling the format engine.
Change the hole. Change the merchandise top. Add extra gadgets. The zigzag holds.









