CSS Columns Masonry Reads in the Wrong Order
A column-count masonry grid shuffles a newest-first post list down each column; a capped CSS grid keeps it in reading order without JavaScript.

The home page of this blog used to look like a tidy masonry wall: four columns of cards with uneven heights, no gaps, no JavaScript. It also lied about the order of the posts.
The list was sorted newest first. Reading the top row left to right gave this:
Aug 16 → Aug 7 → Jul 21 → Jun 26
The second and third newest posts, Aug 15 and Aug 10, were stacked under the first card. Anyone scanning the top row for something recent skipped them entirely.
Columns fill downward first
The wall was built with multi-column layout:
.masonry-grid {
column-count: 4;
column-gap: 1.5rem;
}
.post-card {
break-inside: avoid;
margin-bottom: 1.25rem;
}
Multi-column layout treats its children like text in a newspaper. Content fills the first column top to bottom, then flows into the next. Cards one through three went down column one, card four started column two, and so on. The browser did exactly what the spec says. Technically correct, the worst kind of correct.
The fixes that don’t work
order: it only applies to flex and grid items. Multicol ignores it.- Reordering the list on the server: you’d have to know which card lands in which column. That depends on the column count, which changes at every breakpoint, and on card heights, because the browser balances columns by height.
- A JavaScript masonry library: it works, but it means shipping a script and paying for a layout jump to fix a list of links.
- Native masonry (
display: grid-laneson MDN): still experimental and not Baseline. Maybe later.
A grid that reads like a list
The fix was to give up the staggered heights and use a grid, which places items in rows:
.post-grid {
--gap: 1.5rem;
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(min(100%, max(17.5rem, (100% - 3 * var(--gap)) / 4)), 1fr)
);
gap: var(--gap);
}
The minmax() line packs in three rules:
(100% - 3 * var(--gap)) / 4is the width of one column when four share a row.max(17.5rem, …)means a column is never narrower than 17.5rem, and never narrower than a quarter of the row. So a fifth column can never fit.min(100%, …)stops a single column from overflowing a phone narrower than 17.5rem.
As the screen widens, the grid goes from one column to two, three, then four, and stops there. No media queries needed. Read the first row now:
Aug 16 → Aug 15 → Aug 10 → Aug 7
Making rows look intentional
A grid row is as tall as its tallest card, so uneven covers leave ragged gaps. Two small rules fix that:
.post-card figure {
aspect-ratio: 16 / 10;
}
.post-card img {
width: 100%;
height: 100%;
object-fit: cover;
}
Every cover is now the same shape, and portrait photos get cropped to the middle. Make the card itself a flex column and give the footer margin-top: auto, and the footers line up across a row too.
The entrance animation trap
The first row fades up on load, 80ms apart. The fill mode matters more than it looks:
.post-grid > .post-card:nth-child(-n + 4) {
animation: fadeUp 400ms ease backwards;
}
With both (or forwards), the finished animation keeps applying its last keyframe, transform: translateY(0). Animated values beat normal declarations, so the card’s :hover { transform: translateY(-3px) } lift silently stops working on exactly the cards people see first. In a quick check after finishing the animation, a both card stayed at translateY(0), while a backwards card lifted to -3px.
backwards holds the first keyframe during the delay, then lets go once the animation ends.
If you see this
- The top row skips the newest items: a
column-countlayout is filling columns downward. - You set
orderon cards and nothing moves: the parent isn’t a flex or grid container. - Hover effects work everywhere except the cards that animated in: change the fill mode to
backwards.
References
- W3C: CSS Multi-column Layout Module Level 1
- W3C: CSS Display Module Level 3, the order property
- MDN: Masonry layout
- W3C: CSS Animations Level 1, animation-fill-mode
Cover photo: Unsplash.