A Vue.js Approach To Masonry

How to achieve the masonry layout in Vue.js without absolute positioning or needing the sizes of your images

Tim Gehrsitz
4 min readJan 20, 2023

--

Mansplaining Masonry (And How My Grandmother Taught Me This Recipe)

If you’re creating a photo gallery in Vue, you may have tried the “Masonry” layout, in which photos stack in columns with even spacing between them, and when the number of columns change to fit the window, the images at the top stay at the top. I attempted to use MagicGrid, a Vue-specific package for masonry, but I found that on first load it would stack images over each other (possibly my fault for having it load 4000x6000 scans). Instead, I realized there’s a pretty easy way to (almost) achieve masonry — with minimal explicit javascript.

The Actual Code

<div class="gallery-column" 
v-for="ci in Math.ceil(((windowWidth*0.9)-320) / 400)"
:key="ci">
<img :src="photo.url"
v-on:click="showLargeImage(photo.url)"
v-for="(photo, index) in photos.filter((p,pi) => ((pi+1-ci)%Math.ceil(((windowWidth*0.9)-320) / 400))===0)"
class="gallery-image"
:key="index"/>
</div>

Ignoring my gross calculations and terrible naming conventions… there it is. Simple as. Here’s the breakdown: using a v-for, you create the appropriate number of columns based on the window width and the width of the images. For me, the images were 400px wide so I took the window width and did a math.ceil division by 400 (along with some other calculations to fit with outside borders and the look I wanted) in order to get the number of columns that should show. Within each column, I wanted to generate the appropriate images: column 1 would have the first image, column 2 would have the second, etc., and most importantly, if there were 4 columns, then image 5 would appear as the second image in column 1. V-if didn’t appear to work with v-for, so instead I simply filtered the list of photos for each v-if based on the column, such that column 1 in a 4 column layout would show images 1,5,9,13 in that order, and in a 3 column layout would show images 1,4,7,10.

Math (🤮)

(p,pi) => (pi+1-ci)%Math.ceil(((windowWidth*0.9)-320) / 400) === 0

p=photo, pi=photo index, and ci=column index. To see if we’re in the right column, you take the index of the photo and subtract the index of the column. If that divides evenly by the number of columns, then you’re in the right column. To explain more thoroughly: if a photo is in column c, it will have index c, then c+cn, then c+2cn, etc. where cn is the number of columns, because we will have to place a photo in every other column before returning to column c, hence cn. Thus, you can derive the index of the photos which should be in a column c by saying the photo index, p, minus the index of the column, will be evenly divisible by cn. So we get that p-c%cn = 0. Why +1? When vue does a v-for on a number (v-for=”i in 10") it returns i as 1,2,3… so we account for the fact that it counts starting at 1 rather than 0.

JavaScript (🤮🤮)

The last thing we need to do is get the windowWidth, which is the only javascript we really need. To do this, add the following to your “export default” section (courtesy of Samayo on StackOverflow)

data() {
return {
windowWidth: window.innerWidth,
}
},
methods: {
onResize() {
this.windowWidth = window.innerWidth
},
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize);
},
mounted() {
this.$nextTick(() => {
window.addEventListener('resize', this.onResize);
})
}

Bada-bing, bada-boom, we are all done. Personally, my un-ending and Bruno Mars-esque love for flexbox — where I would catch a grenade for it but it, being a css 3 web layout model, wouldn’t do the same — caused me to make the vertical column flex as well, in which all you really need to do is make sure you justify-content: flex-start so things sit at the top. Another thing you can do is set the width of your columns to 100% so that they’ll resize while the window is resizing before it meets the threshold to add another column.

THE CAVEAT:

So there is one caveat: this isn’t actually masonry. I was lying this whole time. If column 1 has a bunch of images that are 100px tall and column 2 has a bunch of images that are 1000px tall, you’re going to have some lopsided columns that end far, far away from each other. This dumbed down version doesn’t know which column is the shortest, which real masonry would account for. In the future, I’m going to work on a fix to this and see if I can’t fenagle my way out of using too much JavaScript and position: absolute; by either pre-loading the image heights and doing some quick maths, or by just checking the height of each column during each loop, then slapping the v-for-generated images into the next spot of the v-for-generated columns. How that would work, I have no idea, but my goal is to make @LinusBorg ask me to take this down for causing an influx of people using v-for in ways God would disapprove of.

--

--

Tim Gehrsitz
0 Followers

Front-end Developer with a Film Photography Addiction