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 really 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 stream down after which wrap right into a second column. Normally we consider the flex-wrap property by way of 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. You must inform the container “you might be
500pxtall” for wrapping to kick in. That’s brittle. - The tab order breaks. Gadgets stream down the primary column (i.e., 1, 2, 3), then bounce to the second column (i.e., 4, 5, 6). That’s not a waterfall. That’s two buckets.
To be honest, the CSS Grid strategy we’re about to construct has its personal hardcoded worth. We’ll get to that. Nevertheless it sidesteps the Tab order drawback completely, and that’s a significant win.
The Grid Plan
Right here’s what I need to do as an alternative:
- 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 determine 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 really 100px tall — they’re barely taller as soon as the border will get added. This may 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 may attain for .merchandise:nth-of-type(even) right here, and on this demo it might produce the identical outcome since all the youngsters are the identical factor sort. However nth-of-type selects by tag title, not by class. So when you ever combine completely different factor sorts contained in the wrapper, it’ll match in methods you don’t anticipate. :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 occurring and it’s value understanding.
Rework Percentages Are Totally different
Percentages in transforms work fully in a different way than they do anyplace else in CSS.
In stream format, positioned format, or actually any format mode, a proportion refers back to the mum or dad’s obtainable area. In case you write width: 50% on a component inside a wrapper, you’re saying: The container is that this vast. Make me half of that.
Transforms don’t work this fashion. In a rework, percentages consult with the factor itself. So translateY(50%) doesn’t imply “transfer down by half of the obtainable area.” It means “transfer down by half of your individual top.” If the factor is 200px tall, it strikes down by 100px.
That is really the identical coordinate-system conduct you see with the person translate(), scale(), and rotate() CSS properties. All of them are utilized within the factor’s personal coordinate area, post-layout. The browser finishes laying all the things 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 heart, 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 regardless of how tall the gadgets are.
The outcome seems shut. Nevertheless it’s not fairly proper.
The Hole Downside
We are able to expose the imperfection by cranking the hole as much as one thing absurd — say, 100px. After we do, the even gadgets clearly aren’t sitting the place they need to. They should journey slightly additional to account for the vertical area between rows.
Right here’s the repair. First, let’s retailer the hole in a CSS customized property so we will 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 must cowl half the space between rows — the total worth would push it too far.
Set the hole to 16px, it seems nice. Set it to 100px, it nonetheless seems nice. The maths holds whatever the worth.
The Overflow Shock
We’ve solved the core puzzle. However there’s a hidden drawback ready to floor.
Let’s add a border to the wrapper to see its boundaries:
.wrapper {
border: 2px stable purple;
}
With 5 gadgets, all the things seems fantastic. The wrapper incorporates all of its kids. 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 anxious, that sixth merchandise remains to be sitting in its authentic, untranslated place. The wrapper sizes itself based mostly on that authentic place. The rework shifts pixels visually, however the mum or dad has no thought something moved.
We shocked the browser.
The Repair: Reserve the Area
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 mum or dad’s width (not the kid’s top), we will’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 type 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, however it’s value being sincere about.
The wrapper now reserves precisely sufficient area 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 might sound at first look.
Display readers are unaffected. Transforms are purely visible. The DOM order stays 1-6, and that’s precisely how assistive know-how will announce them. No reordering surprises, not like the flexbox column-wrap strategy the place the visible order and DOM order can diverge.
Focus order stays intact, too. When somebody tabs via the gadgets, focus follows the supply order, not the place the gadgets seem visually. In our zigzag, the visible stream and supply order each cascade left-right, top-down, in order that they naturally agree. In case your format ever will get advanced sufficient that visible and supply order begin to diverge, that’s while you’d must assume extra fastidiously about focus administration.
Respect movement preferences. The zigzag itself is static — we’re not animating the rework. However when you ever determine to animate gadgets into their staggered positions (say, on web page load), wrap that animation in a prefers-reduced-motion examine:
/* animates when person has no movement desire */
@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 don't have any desire on movement are the one ones who get the animation. Sometimes, although, you may do the inverse of that. The format nonetheless works both means.
The Remaining Demo
As soon as once more:
Conclusion
The zigzag format is basically simply three concepts stacked on high of one another:
- A two-column grid provides us the muse.
translateY(50%)creates the stagger and works as a result of rework percentages reference the factor itself, not the mum or dad.padding-bottomreserves area 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.
