2

How to achieve a layout with similar properties to Pintrest?

Specifically:

  • Responsive layout where all pictures are the same width.
  • All pictures are contained in a single div. There are no multiple containers trying to replicate "columns", e.g. using flexbox.
  • The order of the layout is such that there are no gaps. For example, if the layout is two columns, and the pictures are of width, heights: [(100,400), (100,100), (100,400), (100,100), (100,400), (100,100)], the columns will be around the same height and not 1200 for the left and 400 for the right. The screenshots below illustrate this.

Here is a screenshot showing that all photos are in a single div container. enter image description here

The responsive layout: enter image description here

An example of the third bullet point: the dark shower picture went below the hanging seat picture and not the external house picture. This is because the hanging seat picture has a smaller height. enter image description here

Similarly when the layout is two columns wide: the picture of the bedroom goes below the handing chairs because the hanging seat picture has a smaller height. enter image description here

Although this post appears similar to CSS-only masonry layout, the solutions do not result in desired outcome:

Using @Oliver Joseph Ash solution results in:

grid-container {
  display: flex;
  flex-flow: column wrap;
  align-content: space-between;
  /* Your container needs a fixed height, and it 
   * needs to be taller than your tallest column. */
  height: 960px;
  /* Optional */
  background-color: #f7f7f7;
  border-radius: 3px;
  padding: 20px;
  width: 60%;
  margin: 40px auto;
  counter-reset: items;
}

grid-item {
  width: 24%;
  /* Optional */
  position: relative;
  margin-bottom: 2%;
  border-radius: 3px;
  background-color: #a1cbfa;
  border: 1px solid #4290e2;
  box-shadow: 0 2px 2px rgba(0, 90, 250, 0.05), 0 4px 4px rgba(0, 90, 250, 0.05), 0 8px 8px rgba(0, 90, 250, 0.05), 0 16px 16px rgba(0, 90, 250, 0.05);
  color: #fff;
  padding: 15px;
  box-sizing: border-box;
}
<grid-container>
  <grid-item>
    <a href="https://g.foolcdn.com/image/?url=https%3A%2F%2Fg.foolcdn.com%2Feditorial%2Fimages%2F492452%2Fgettyimages-1014028076.jpg&w=700&op=resize">
      <img src="https://g.foolcdn.com/image/?url=https%3A%2F%2Fg.foolcdn.com%2Feditorial%2Fimages%2F492452%2Fgettyimages-1014028076.jpg&w=700&op=resize">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://image.jimcdn.com/app/cms/image/transf/none/path/sa716b1500dd60f05/image/ic839a74ed6a8a054/version/1519833130/image.jpg">
      <img src="https://image.jimcdn.com/app/cms/image/transf/none/path/sa716b1500dd60f05/image/ic839a74ed6a8a054/version/1519833130/image.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://c8.alamy.com/comp/AXBEXR/stock-photograph-of-a-asian-teen-with-a-trumpet-to-her-ear-AXBEXR.jpg">
      <img src="https://c8.alamy.com/comp/AXBEXR/stock-photograph-of-a-asian-teen-with-a-trumpet-to-her-ear-AXBEXR.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://thumbs.dreamstime.com/z/cyber-woman-orange-11363555.jpg">
      <img src="https://thumbs.dreamstime.com/z/cyber-woman-orange-11363555.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcThObqAcFeb4byMcwLVkU1JVMYonpavYmEukk9r3rqF2oBTnd1q">
      <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcThObqAcFeb4byMcwLVkU1JVMYonpavYmEukk9r3rqF2oBTnd1q">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://www.demilked.com/magazine/wp-content/uploads/2018/03/5aaa1cce4180b-funny-weird-wtf-stock-photos-57-5a3bb7ba3c266__700.jpg">
      <img src="https://www.demilked.com/magazine/wp-content/uploads/2018/03/5aaa1cce4180b-funny-weird-wtf-stock-photos-57-5a3bb7ba3c266__700.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-5-5b0801c7504b2__700.jpg">
      <img src="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-5-5b0801c7504b2__700.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-8-5b0801cd0f33d__700.jpg">
      <img src="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-8-5b0801cd0f33d__700.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://www.atomix.com.au/media/2017/07/StockPhotoBanner.jpg">
      <img src="https://www.atomix.com.au/media/2017/07/StockPhotoBanner.jpg">
    </a>
  </grid-item>
</grid-container>

Which makes the output look like: enter image description here

Whereas @Michael_B solution:

grid-container {
  display: grid;
  grid-auto-rows: 50px;
  grid-gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
}

grid-item {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3em;
  font-weight: bold;
  color: white;
}

img {
  width: 200px;
}
<grid-container>
  <grid-item>
    <a href="https://g.foolcdn.com/image/?url=https%3A%2F%2Fg.foolcdn.com%2Feditorial%2Fimages%2F492452%2Fgettyimages-1014028076.jpg&w=700&op=resize">
      <img src="https://g.foolcdn.com/image/?url=https%3A%2F%2Fg.foolcdn.com%2Feditorial%2Fimages%2F492452%2Fgettyimages-1014028076.jpg&w=700&op=resize">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://image.jimcdn.com/app/cms/image/transf/none/path/sa716b1500dd60f05/image/ic839a74ed6a8a054/version/1519833130/image.jpg">
      <img src="https://image.jimcdn.com/app/cms/image/transf/none/path/sa716b1500dd60f05/image/ic839a74ed6a8a054/version/1519833130/image.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://c8.alamy.com/comp/AXBEXR/stock-photograph-of-a-asian-teen-with-a-trumpet-to-her-ear-AXBEXR.jpg">
      <img src="https://c8.alamy.com/comp/AXBEXR/stock-photograph-of-a-asian-teen-with-a-trumpet-to-her-ear-AXBEXR.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://thumbs.dreamstime.com/z/cyber-woman-orange-11363555.jpg">
      <img src="https://thumbs.dreamstime.com/z/cyber-woman-orange-11363555.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcThObqAcFeb4byMcwLVkU1JVMYonpavYmEukk9r3rqF2oBTnd1q">
      <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcThObqAcFeb4byMcwLVkU1JVMYonpavYmEukk9r3rqF2oBTnd1q">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://www.demilked.com/magazine/wp-content/uploads/2018/03/5aaa1cce4180b-funny-weird-wtf-stock-photos-57-5a3bb7ba3c266__700.jpg">
      <img src="https://www.demilked.com/magazine/wp-content/uploads/2018/03/5aaa1cce4180b-funny-weird-wtf-stock-photos-57-5a3bb7ba3c266__700.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-5-5b0801c7504b2__700.jpg">
      <img src="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-5-5b0801c7504b2__700.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-8-5b0801cd0f33d__700.jpg">
      <img src="https://static.boredpanda.com/blog/wp-content/uploads/2018/05/emilia-clarke-making-stock-photos-8-5b0801cd0f33d__700.jpg">
    </a>
  </grid-item>
  <grid-item>
    <a href="https://www.atomix.com.au/media/2017/07/StockPhotoBanner.jpg">
      <img src="https://www.atomix.com.au/media/2017/07/StockPhotoBanner.jpg">
    </a>
  </grid-item>
</grid-container>

Is a lot better but results in height overlapping pictures: enter image description here

wohlstad
  • 12,661
  • 10
  • 26
  • 39
Greg
  • 8,175
  • 16
  • 72
  • 125
  • Are the heights of the images in some way constrained? E.g. are they multiples of a certain size/height? If they are completely "random" heights, there is no way to achieve a masonry layout with just CSS, you need a small row height and JavaScript to dynamically evaluate the height of the picture and then set a row span to match the height on each grid item... – exside Jul 29 '19 at 03:09
  • @exside there are no height constraints at the moment, although I can resize images to a restricted range if needed. For example to force a picture to have a height of `[400px, 420px, 440px,..., 780px, 800px] `. Is that set up achievable? – Greg Jul 29 '19 at 03:19
  • Technically yes, it's all about the granularity you need/want to have in the heights of the images, I mean you can make 100s of CSS classes to match every single row-span, but I would say there's a limit to that approach where it gets kinda ridiculous =) and you are better going with a few lines of JS to do it...so I would probably not go further than having something like 6-12 "span" classes, e.g. you could say the row height is something like 120px and then you go from there, e.g. `.span-2` would be 240px, `.span-3` 360px and so on, height range from 120-1440px should be enough =)... – exside Jul 29 '19 at 03:28
  • The "problem" with this approach though is that you need to know the height of your images up-front to set the appropriate span-classes, depending on how you get the images and how you render them into HTML this is easy or not... – exside Jul 29 '19 at 03:31
  • You mentioned that it could be done in a few lines of JS, would it be possible to get some idea of how/what that involves? – Greg Jul 29 '19 at 04:52

1 Answers1

2

My recommendation would be to set up a few "span" classes and contrain the images/masonry-items to have a height that is a multiple of the smallest unit. This approach allows for a CSS only masonry, but we need to know what multiple of the smallest unit items are beforehand (e.g. while rendering the HTML) so we can apply these span classes that could look like this:

.grid-span-2 {
  grid-row-end: span 2;
}
.grid-span-3 {
  grid-row-end: span 3;
}
.grid-span-4 {
  grid-row-end: span 4;
}
...

If this is not possible, the next best approach is to use a little JS to dynamically calculate the row-span based on an items height. The snippet below should give an idea of how to approach this.

The most important part is the following:

    const sizeItems = () => {
        // only trigger DOM repaint when necessary
        if ( $('.grid-container')[0].style.gridAutoRows !== '1px' ) {
            $('.grid-container')[0].style.gridAutoRows = '1px';
        }

        $('.grid-item > *').forEach(el => {
            const height = Math.floor(el.getBoundingClientRect().height);
            // only trigger DOM repaint when necessary
            if ( el.parentElement.style.gridRowEnd !== `span ${height}` ) {
                el.parentElement.style.gridRowEnd = `span ${height}`;
            }
        });
    }

We need to wait for images to load before we can do that, and also do it again when the viewport is resized, that's basically the rest of the JS code.

What we're doing here is to get the real/true height of the content within a grid-item. I prefer to use a wrapper element inside each grid-item and use that to measure. Why?

a) There are properties from the grid-module that can mess with the actual height of the grid-item without you seeing it and can then lead to incorrect height-calculations.

b) With this approach we can't use grid-gap (because in this extreme case we are making 1px-rows for the most precise sizing) as it would mess up the cell-heights, so to still be able to have a gap, we can use this wrapper div as well and apply padding to it to imitate the grid-gap.

For the JS approach we set grid-auto-rows: 1px;, this gives us the most flexibility in terms of item height, as basically each grid-cell can be sized to fit its content exactly, note though that there can be some small glitches of 1px, most likely due to subpixel rendering...

// micro DOM helper, just for convenience
const $ = window.$ = function(a,b){return Array.prototype.slice.call(document.querySelectorAll(a, b));};

const sizeItems = () => {
 // only trigger DOM repaint when necessary
 if ( $('.grid-container')[0].style.gridAutoRows !== '1px' ) {
  $('.grid-container')[0].style.gridAutoRows = '1px';
 }
 
 $('.grid-item > *').forEach(el => {
  const height = Math.floor(el.getBoundingClientRect().height);
  // only trigger DOM repaint when necessary
  if ( el.parentElement.style.gridRowEnd !== `span ${height}` ) {
   el.parentElement.style.gridRowEnd = `span ${height}`;
  }
 });
}

Promise.all($('img').map(img => { 
 return new Promise(resolve => {
  img.onload = () => {
   resolve();
  }
  img.onerror = () => {
   resolve();
  }
 });
})).then(() => {
 const emit = new Event('images');
 window.dispatchEvent(emit);
});

window.addEventListener('images', function(e) {
 console.log('onImagesLoaded');
 sizeItems();
});

window.addEventListener('load', function(e) {
 console.log('onLoad');
 sizeItems();
});

window.addEventListener('resize', function(e) {
 console.log('onResize');
 sizeItems();
});
* {
  box-sizing: border-box;
  margin: 0;
}
body {
  font-family: sans-serif;
}

.grid-container {
  background: gold;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(100%/3), 1fr));
  grid-gap: 0; /* can't use grid gap! */
  /* grid-auto-rows: auto; */
  grid-auto-flow: dense;
  /* if you don't want a gap on the outside of the grid when working with padding */
  /* margin: -4px; */
}
@media screen and (max-width: 600px) {
  .grid-container {
    grid-template-columns: repeat(auto-fill, minmax(50%, 1fr));
  }
}
@media screen and (max-width: 400px) {
  .grid-container {
    display: block;
  }
}

.grid-item {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  font-size: 1.3em;
  font-weight: bold;
  color: hsl(0,0%,14%);
}

.grid-gap {
  /* if you need a grid-gap, apply padding to a wrapper element INSIDE the grid-item */
  /* padding: 4px; */
  overflow: hidden;
  width: 100%;
}

.grid-item a,
.grid-item img {
  display: block;
  width: 100%; height: 100%;
}

.grid-item figcaption {
  background: lightcoral;
  color: white;
  font-size: 14px;
  font-style: italic;
  padding: 4px 0;
  text-align: center;
}
<div class="grid-container">
 <div class="grid-item">
  <div class="grid-gap">
   <figure>
    <a href="https://via.placeholder.com/397x625">
     <img src="https://via.placeholder.com/397x625/255a60/FFFFFF" />
    </a>
    <figcaption>This is a caption!</figcaption>
   </figure>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/678x765">
    <img src="https://via.placeholder.com/678x765/4496a1/FFFFFF" />
   </a>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/567x987">
    <img src="https://via.placeholder.com/567x987/eaf8ab/FFFFFF" />
   </a>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/968x956">
    <img src="https://via.placeholder.com/968x956/ccd47e/FFFFFF" />
   </a> 
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/987x687">
    <img src="https://via.placeholder.com/987x687/255a60/FFFFFF" />
   </a>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/620x345">
    <img src="https://via.placeholder.com/620x345/255a60/FFFFFF" />
   </a>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/543x789">
    <img src="https://via.placeholder.com/543x789/4496a1/FFFFFF" />
   </a>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/376x274">
    <img src="https://via.placeholder.com/376x274/eaf8ab/FFFFFF" />
   </a>
  </div>
 </div>
 <div class="grid-item">
  <div class="grid-gap">
   <a href="https://via.placeholder.com/468x623">
    <img src="https://via.placeholder.com/468x623/9fb828/FFFFFF" />
   </a> 
  </div>
 </div>
</div>
exside
  • 3,736
  • 1
  • 12
  • 19
  • For some reason when images are used gaps still appear with captions: https://i.imgur.com/6GVDgNk.png and without captions: https://i.imgur.com/VHTOXkI.png. If instead the photos ``are replaced with a `
    ` element, where `X` is an int representing different height, it works as intended. Not sure why that is?
    – Greg Jul 30 '19 at 09:18
  • Did you use the same/similar markup as in my snippet and if you changed it, did you adapt the JS code? The captions should not make a difference, as long as you use a wrapper to measure the height. I noticed that sometimes it took several attempts in the snippets until it would work, but thought it might be an issue with the SO snippet embed... – exside Jul 30 '19 at 10:13
  • Used the code provided with the only difference that I linked to images on disk, trying exactly the same code (with placeholders) on jsfiddle yields the same results though: https://jsfiddle.net/6953fude/ – Greg Jul 30 '19 at 13:07
  • @Greg I updated the JS code in the snippet above, it now emits a custom event when images are loaded, so we can add an eventlistener for that. But I think the `onload` event for images might not trigger if images are cached already, so we need to add some other listeners as well and basically execute `sizeItems()` already before any event, just to make sure... can you check again with the updated code? – exside Jul 30 '19 at 14:10
  • Unfortunately there are still some gaps: https://i.imgur.com/pnesget.png – Greg Jul 31 '19 at 09:05
  • can you make a fiddle with the exact code you are using? – exside Jul 31 '19 at 12:46
  • It's really strange, I can reproduce it on jsfiddle, but on jsbin it seems to work with the same code https://jsbin.com/haqigoyika/edit?console,output ... also works when I try it locally...trying to debug the jsfiddle...it's a bit annoying, no console (or I don't know how to enable it) =)... – exside Jul 31 '19 at 14:41
  • @Greg it's the jsfiddle "load type" of JS! By default is is "on load"...that won't work, if you set it to any other option, it works... so if it does not work in your code as well, where and how to you initialize the javascript in your "real-world" code? Did you try wrapping the JS code above in an IIFE? e.g. `(function() { // code })();` – exside Jul 31 '19 at 14:47
  • I tried wrapping it and the outcome was the same. This is a Flask application which has the java script and css files linked in the following ways: `` and `` – Greg Aug 01 '19 at 09:17
  • you can't "link" a JS file with the `` tag, you would need to include it ideally before the closing `

    ` tag with a ``!

    – exside Aug 01 '19 at 20:38
  • Silly mistake, that did it! Thank you – Greg Aug 02 '19 at 07:52