I'm trying to code a masonry layout (please don't suggest using a library and yes, I know about the new Firefox CSS masonry feature, but I need better support) starting from a CSS grid layout.
The HTML looks something like this, a section with a bunch of images of various aspect ratios:
<section class='grid--masonry'>
<img src='black_cat.jpg' alt='black cat'/>
<!-- and so on, more images following the first -->
</section>
The styling is pretty basic, just setting up the grid:
$w: Min(8em, 100%);
$s: .5rem;
.grid--masonry {
display: grid;
grid-template-columns: repeat(auto-fit, $w);
grid-gap: $s;
padding: $s;
> * { width: $w }
}
In the JS, the first step is to get the grid, its element nodes (filtering out text nodes), the grid gap and init the number of columns:
Then, after the page has loaded, I call the layout() function:
addEventListener('load', e => {
layout(); /* initial load */
addEventListener('resize', layout, false) /* on resize */
}, false)
The layout() function does the following:
- gets the
leftoffset for every grid item - since these offsets are identical for items on the same column, it creates a set of offsets whose size is the current number of columns
- if the current number of columns is identical to the
grid.ncolwe already have, we do nothing and exit the function - otherwise, we update the
grid.ncolvalue and then... - remove any top margin we may have on the grid items,
- check whether we have more than one column
- if we only have one column, we just exit the
layout()function, we're done! - otherwise, for each column, we get the items on it (filtering by offset)
- if we only have one row, we exit the
layout()function and do nothing more - otherwise, we loop through the items on the current column, get the bottom edge of the previous item and the top edge of the current one and compute by how much we need to shift the current one up
Here's the code doing just that:
function layout() {
/* get left offset for every item of the grid */
grid.items.forEach(c => { c.off = c._el.getBoundingClientRect().left });
/* make a set out of the array of offsets we've just obtained */
grid.off = new Set(grid.items.map(c => c.off));
/* if the number of columns has changed */
if(grid.ncol !== grid.off.size) {
/* update number of columns */
grid.ncol = grid.off.size;
/* revert to initial positioning, no margin */
grid.items.forEach(c => c._el.style.removeProperty('margin-top'));
/* if we have more than one column */
if(grid.ncol > 1) {
grid.off.forEach(o => { /* for each column */
/* get items on that column */
let col_items = grid.items.filter(c => c.off === o),
col_len = col_items.length;
/* if we have more than 1 item per column */
if(col_len > 1) {
for(let i = 1; i < col_len; i++) {
let prev_fin = col_items[i - 1]._el.getBoundingClientRect().bottom /* bottom edge of item above */,
curr_ini = col_items[i]._el.getBoundingClientRect().top /* top edge of current item */;
col_items[i]._el.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`
}
}
})
}
}
}
I would have hoped this works, but it doesn't. It seems to work for two columns, but not for more. And I just can't figure out why.
let grid = { _el: document.querySelector('.grid--masonry'), ncol: 0 };
grid.items = [...grid._el.childNodes].filter(c => c.nodeType === 1).map(c => ({ _el: c }));
grid.gap = parseFloat(getComputedStyle(grid._el).gridRowGap);
function layout() {
/* get left offset for every item of the current grid */
grid.items.forEach(c => { c.off = c._el.getBoundingClientRect().left });
/* make a set out of the array of offsets we've just obtained */
grid.off = new Set(grid.items.map(c => c.off));
/* if the number of columns has changed */
if(grid.ncol !== grid.off.size) {
/* update number of columns */
grid.ncol = grid.off.size;
/* revert to initial positioning, no margin */
grid.items.forEach(c => c._el.style.removeProperty('margin-top'));
/* if we have more than one column */
if(grid.ncol > 1) {
grid.off.forEach(o => { /* for each column */
/* get items on that column */
let col_items = grid.items.filter(c => c.off === o),
col_len = col_items.length;
/* if we have more than 1 item per column */
if(col_len > 1) {
for(let i = 1; i < col_len; i++) {
let prev_fin = col_items[i - 1]._el.getBoundingClientRect().bottom /* bottom edge of item above */,
curr_ini = col_items[i]._el.getBoundingClientRect().top /* top edge of current item */;
col_items[i]._el.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`
}
}
})
}
}
}
addEventListener('load', e => {
layout(); /* initial load */
addEventListener('resize', layout, false) /* on resize */
}, false);
.grid--masonry {
display: grid;
grid-template-columns: repeat(auto-fit, Min(8em, 100%));
justify-content: center;
grid-gap: .5rem;
padding: .5rem;
background: violet;
}
.grid--masonry > * {
width: Min(8em, 100%);
}
<section class='grid--masonry'>
<img src='https://images.unsplash.com/photo-1510137600163-2729bc6959a6?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=900&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1587041403375-ddce288f4c49?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1580697895575-883f7c755346?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt=''/>
<img src='https://images.unsplash.com/photo-1581200459935-685903de7d62?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1578264050450-ccc2f77796a1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1557153921-10129d0f5b6c?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1584049086295-9f2af90efbb4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1572196663741-b91b8f045330?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1558288215-664da65499af?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1592296109897-9c4d8e490e7a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1525104885119-8806dd94ad58?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1576532116216-84f6a0aedaf6?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1533629947587-7b04aaa0e837?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1568386895623-74df8a7406f0?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1573777058681-73b866833d90?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1552566852-06d10a5050f4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1576759470820-77a440a4d45b?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1586891622678-999a4419da34?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1584797318381-5958ca2e6b39?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1529093589387-b486dcc37c15?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1587421803669-b403d010dd80?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1570458436416-b8fcccfe883f?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
<img src='https://images.unsplash.com/photo-1518206245806-5c1f4d0c5a2a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ' alt='black cat'/>
</section>