CSS-only masonry layout
Asked Answered
E

6

241

I need to implement a masonry layout. However, for a number of reasons I don't want to use JavaScript to do it.

A grid of multiple columns of rectangles of varying height.

Parameters:

  • All elements have the same width
  • Elements have a height that cannot be calculated server side (an image plus various amounts of text)
  • I can live with a fixed number of columns if I have to

there is a trivial solution to this that works in modern browsers, the column-count property.

The problem with that solution is that elements are ordered in columns:

Starting from the top leftmost box, they're numbered 1 through 4 straight down, the topmost box in the next column is 5, and so on.

While I need the elements to be ordered in rows, at least approximately:

Starting from the top leftmost box, they're numbered 1 through 6 straight across, but because box 5 is the shortest the box underneath it is 7 as it has the appearance of being on a row higher than the next box on the far left.

Approaches I've tried that don't work:

Now I could change the server side rendering and reorder the items dividing the number of items by the number of columns, but that's complicated, error-prone (based on how browsers decide to split the item list into columns), so I'd like to avoid it if possible.

Is there some flexbox magic that makes this possible?

Electorate answered 5/6, 2017 at 20:41 Comment(4)
Can't think of a way that does not depend on predefined heights. If you reconsider JS, have a look at #13519153 where i implement such a solution that is quite simple.Ruralize
I realize that you said CSS-only. I just want to mention that Masonry no longer requires jQuery - the minified library is under 8kb - and can be initialized with html alone. Just for reference jsfiddle.net/wp7kuk1tCleodell
If you can determine the height of the elements ahead of time, by knowing the line-height, font-size (you'd have to serve a specific font and do some clever calculations), image height, verticle margin and padding, you can do this. Otherwise, you cannot do this using only CSS. You could also use something like PhantomJS to pre-render each element and get the height of that element, but there would be significant overhead/latency added.Popularize
Almost all possible masonry layouts can be found here. Note that there are also js solutions.Crownpiece
O
267

2021 Update

CSS Grid Layout Level 3 includes a masonry feature.

Code will look like this:

grid-template-rows: masonry
grid-template-columns: masonry

As of March 2021, it's only available in Firefox (after activating the flag).

end update; original answer below


Flexbox

A dynamic masonry layout is not possible with flexbox, at least not in a clean and efficient way.

Flexbox is a one-dimensional layout system. This means it can align items along horizontal OR vertical lines. A flex item is confined to its row or column.

A true grid system is two-dimensional, meaning it can align items along horizontal AND vertical lines. Content items can span across rows and columns simultaneously, which flex items cannot do.

This is why flexbox has a limited capacity for building grids. It's also a reason why the W3C has developed another CSS3 technology, Grid Layout.


row wrap

In a flex container with flex-flow: row wrap, flex items must wrap to new rows.

This means that a flex item cannot wrap under another item in the same row.

Notice above how div #3 wraps below div #1, creating a new row. It cannot wrap beneath div #2.

As a result, when items aren't the tallest in the row, white space remains, creating unsightly gaps.


column wrap

If you switch to flex-flow: column wrap, a grid-like layout is more attainable. However, a column-direction container has four potential problems right off the bat:

  1. Flex items flow vertically, not horizontally (like you need in this case).
  2. The container expands horizontally, not vertically (like the Pinterest layout).
  3. It requires the container to have a fixed height, so the items know where to wrap.
  4. As of this writing, it has a deficiency in all major browsers where the container doesn't expand to accommodate additional columns.

As a result, a column-direction container is not an option in this case, and in many other cases.


CSS Grid with item dimensions undefined

Grid Layout would be a perfect solution to your problem if the various heights of the content items could be pre-determined. All other requirements are well within Grid's capacity.

The width and height of grid items must be known in order to close gaps with surrounding items.

So Grid, which is the best CSS has to offer for building a horizontally-flowing masonry layout, falls short in this case.

In fact, until a CSS technology arrives with the ability to automatically close the gaps, CSS in general has no solution. Something like this would probably require reflowing the document, so I'm not sure how useful or efficient it would be.

You'll need a script.

JavaScript solutions tend to use absolute positioning, which removes content items from the document flow in order to re-arrange them with no gaps. Here are two examples:

Masonry is a JavaScript grid layout library. It works by placing elements in optimal position based on available vertical space, sort of like a mason fitting stones in a wall.

source: http://masonry.desandro.com/

[Pinterest] really is a cool site, but what I find interesting is how these pinboards are laid out... So the purpose of this tutorial is to re-create this responsive block effect ourselves...

source: https://benholland.me/javascript/2012/02/20/how-to-build-a-site-that-works-like-pinterest.html


CSS Grid with item dimensions defined

For layouts where the width and height of content items are known, here's a horizontally-flowing masonry layout in pure CSS:

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

[short] {
  grid-row: span 1;                                             /* 5 */
  background-color: green;
}

[tall] {
  grid-row: span 2;
  background-color: crimson;
}

[taller] {
  grid-row: span 3;
  background-color: blue;
}

[tallest] {
  grid-row: span 4;
  background-color: gray;
}

grid-item {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3em;
  font-weight: bold;
  color: white;
}
<grid-container>
  <grid-item short>01</grid-item>
  <grid-item short>02</grid-item>
  <grid-item tall>03</grid-item>
  <grid-item tall>04</grid-item>
  <grid-item short>05</grid-item>
  <grid-item taller>06</grid-item>
  <grid-item short>07</grid-item>
  <grid-item tallest>08</grid-item>
  <grid-item tall>09</grid-item>
  <grid-item short>10</grid-item>
  <grid-item tallest>etc.</grid-item>
  <grid-item tall></grid-item>
  <grid-item taller></grid-item>
  <grid-item short></grid-item>
  <grid-item short></grid-item>
  <grid-item short></grid-item>
  <grid-item short></grid-item>
  <grid-item tall></grid-item>
  <grid-item short></grid-item>
  <grid-item taller></grid-item>
  <grid-item short></grid-item>
  <grid-item tall></grid-item>
  <grid-item short></grid-item>
  <grid-item tall></grid-item>
  <grid-item short></grid-item>
  <grid-item short></grid-item>
  <grid-item tallest></grid-item>
  <grid-item taller></grid-item>
  <grid-item short></grid-item>
  <grid-item tallest></grid-item>
  <grid-item tall></grid-item>
  <grid-item short></grid-item>
</grid-container>

jsFiddle demo


How it works

  1. Establish a block-level grid container. (inline-grid would be the other option)

  2. The grid-auto-rows property sets the height of automatically generated rows. In this grid each row is 50px tall.

  3. The grid-gap property is a shorthand for grid-column-gap and grid-row-gap. This rule sets a 10px gap between grid items. (It doesn't apply to the area between items and the container.)

  4. The grid-template-columns property sets the width of explicitly defined columns.

    The repeat notation defines a pattern of repeating columns (or rows).

    The auto-fill function tells the grid to line up as many columns (or rows) as possible without overflowing the container. (This can create a similar behavior to flex layout's flex-wrap: wrap.)

    The minmax() function sets a minimum and maximum size range for each column (or row). In the code above, the width of each column will be a minimum of 30% of the container and maximum of whatever free space is available.

    The fr unit represents a fraction of the free space in the grid container. It's comparable to flexbox's flex-grow property.

  5. With grid-row and span we're telling grid items how many rows they should span across.

Outspoken answered 19/7, 2017 at 21:3 Comment(8)
Fantastic answer! With the "CSS Grid with item dimensions defined" solution, is it possible to express the height of a cell as a percentage of the cell width? This would be useful for displaying images, where the aspect ratio is known. We want to maintain the aspect ratio at all times.Emrich
Thanks. With respect to the aspect ratio issue for images, the "percentage of cell width" method (the percentage padding trick?), it is not reliable in Grid, or Flexbox, for that matter. See here and here. @OliverJosephAshOutspoken
Yes, I am referring to the percentage padding trick. Is it possible to use any of the workarounds in combination with your masonry layout solution? For context, we are looking to implement a CSS only masonry layout for the images on unsplash.com.Emrich
@OliverJosephAsh, the percentage padding trick, used with grid / flex items, fails in Firefox (and possibly other browsers). See the link references in my previous comment for the reasons. I would say JS is currently the safest route.Outspoken
Unfortunately using JS for this is not an option. We want to enable server side rendering for performance and SEO reasons. This means the layout must be rendered before client-side JavaScript has downloaded, parsed, and executed. Given this seems to be impossible, I guess we'll have to make a trade off somewhere along the line! Thanks for your help :-)Emrich
Spec Update: In flexbox, percentage margins and paddings are now set to resolve again the container's inline size. drafts.csswg.org/css-flexbox/#item-margins @OliverJosephAshOutspoken
How did you arrive at the 30% value in the minmax(30%, 1fr) expression? It seems arbitrary - it's not quite a third? If I use something closer to a third, it doesn't work - and if I use a lower value, things fall apart as well. How come 30% happens to work? It seems to be dependent on grid-gap as well - it falls apart with larger values. How would you pick the correct value for a different number of columns? Is there any way to reliably calculate this value?Dayledaylight
As of July 2022 no luck with the masonry feature being supported by any browser.Harber
E
27

This is recently discovered technique involving flexbox: https://tobiasahlin.com/blog/masonry-with-css/.

The article makes sense to me, but I haven't tried to use it, so I don't know if there are any caveats, other than mentioned in Michael's answer.

Here's a sample from the article, making use of the order property, combined with :nth-child.

Stack snippet

.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;
}

.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;
}

 /* Just to print out numbers */
div.item::before {
  counter-increment: items;
  content: counter(items);
}

/* Re-order items into 3 rows */
.item:nth-of-type(4n+1) { order: 1; }
.item:nth-of-type(4n+2) { order: 2; }
.item:nth-of-type(4n+3) { order: 3; }
.item:nth-of-type(4n)   { order: 4; }

/* Force new columns */
.break {
  flex-basis: 100%;
  width: 0;
  border: 1px solid #ddd;
  margin: 0;
  content: "";
  padding: 0;
}

body { font-family: sans-serif; }
h3 { text-align: center; }
<div class="container">
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 190px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 120px"></div>
  <div class="item" style="height: 160px"></div>
  <div class="item" style="height: 180px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 150px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 190px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 120px"></div>
  <div class="item" style="height: 160px"></div>
  <div class="item" style="height: 180px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 150px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 170px"></div>
  
  <span class="item break"></span>
  <span class="item break"></span>
  <span class="item break"></span>
</div>
Emrich answered 15/6, 2019 at 16:33 Comment(7)
First off, link only answers is bad ones, as when such link dies, so does the answer. Second, if you read the given answer more carefully, you'll find what's mentioned in your link is covered. Simply put, there are too many limitations using Flexbox alone, unless what is mentioned above is not an issue.Eades
I have read the accepted answer very thoroughly—you'll even see some comments I added to the bottom of it. The flexbox approach I linked to is unique and not covered by that answer. I don't want to repeat the article, because it's very complicated, and the article does a great job of covering that.Emrich
The only thing that the linked does different is making use of the order property, making it look like items flows from left to right. Still, its main trick is flex-flow: column wrap, which is mentioned in the above answer. In addition, it can't flow the items in the way a real masonry does, e.g. if the 3rd and 4th item would fit in the 3rd column, they would, the linked one doesn't. But again as I said, it all depends on what the requirements are, and in this case (this question), it won't work as what asked for.Eades
Furthermore, it is not about "you don't want to repeat the article", an answer should contain the essential part of the code, so it will still be valid when the linked resource dies.Eades
Fair points. I thought it would be useful to share this article here anyway, since it builds upon the other techniques mentioned previously, and whilst it doesn't answer the original question, it might help others for whom these limitations are acceptable. I'm happy to delete my answer and re-share the link as a comment, noting the key difference (order).Emrich
It is useful, though as a link alone, do it as a comment, if you edit in the code, keep it as an answer.Eades
This answer needs the flex element to be a fixed height, so it's a bit useless with dynamic content.Enforcement
C
6

I find This solution, It is most probably compatible with all browsers.

Note: if anyone finds any error or browser support issues. please update this answer Or comments

CSS Profpery Support reference:

column-count, gap, fill break-inside, page-break-inside

Note: page-break-inside This property has been replaced by the break-inside property.

  

.container {
  -moz-column-count: 1;
       column-count: 1;
  -moz-column-gap: 20px;
       column-gap: 20px;
  -moz-column-fill: balance;
       column-fill: balance;
  margin: 20px auto 0;
  padding: 2rem;
}
.container .item {
  display: inline-block;
  margin: 0 0 20px;
  page-break-inside: avoid;
  -moz-column-break-inside: avoid;
       break-inside: avoid;
  width: 100%;
}
.container .item img {
  width: 100%;
  height: auto;
}
@media (min-width: 600px) {
  .container {
    -moz-column-count: 2;
         column-count: 2;
  }
}
@media (min-width: 900px) {
  .container {
    -moz-column-count: 3;
         column-count: 3;
  }
}
@media (min-width: 1200px) {
  .container {
    -moz-column-count: 4;
         column-count: 4;
  }
}
   
 CSS-Only Masonry Layout 
  
<div class="container">
  <div class="item"><img src="https://placeimg.com/600/400/animals" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/600/arch" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/300/nature" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/450/people" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/350/tech" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/800/animals/grayscale" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/650/arch/sepia" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/300/nature/grayscale" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/400/people/sepia" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/600/tech/grayscale" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/200/animals/sepia" alt=""></div>
  <div class="item"><img src="https://placeimg.com/600/700/arch/grayscale" alt=""></div>
</div>
Cernuous answered 7/12, 2021 at 12:2 Comment(4)
Yeah but if you read the question the goal was to achieve this row-wise. The OP already knew the column-wise solution, which is easy.Smithy
Perhaps some explanation and compatibility commentsMick
@Mick all browser supports it. column-count, gap, fill break-inside, page-break-inside Note: This property has been replaced by the break-inside property.Larochelle
So please update your answer with this informationMick
N
4

Finally a CSS-only solution to easily create masonry layout but we need to be patient since there is no support for it for now.

This feature was introduced in the CSS Grid Layout Module Level 3

This module introduces masonry layout as an additional layout mode for CSS Grid containers.

Then

Masonry layout is supported for grid containers by specifying the value masonry for one of its axes. This axis is called the masonry axis, and the other axis is called the grid axis.

A basic example would be:

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-template-rows: masonry; /* this will do the magic */
  grid-gap: 10px;
}

img {
  width: 100%;
}
<div class="container">
  <img src="https://picsum.photos/id/1/200/300">
  <img src="https://picsum.photos/id/17/200/400">
  <img src="https://picsum.photos/id/18/200/100">
  <img src="https://picsum.photos/id/107/200/200">
  <img src="https://picsum.photos/id/1069/200/600">
  <img src="https://picsum.photos/id/12/200/200">
  <img src="https://picsum.photos/id/130/200/100">
  <img src="https://picsum.photos/id/203/200/100">
  <img src="https://picsum.photos/id/109/200/200">
  <img src="https://picsum.photos/id/11/200/100">
</div>

This will produce the following result on Firefox if you enable the feature like explained here: https://caniuse.com/?search=masonry

  1. open Firefox and write about:config in the url bar
  2. do a search using masonry
  3. you will get one flag, make it true

CSS masonry layout

If we reduce the screen screen, the reponsive part is perfect!

Masonry layout using CSS only

Nikolaos answered 3/3, 2021 at 12:54 Comment(1)
It doesn't seem to work in your snippetEpps
L
3

here's the responsive way using css columns.
Note: elements are ordered by column and not by row.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css">
    <title>Responsive Grid Layout</title>
</head>
<body>
    <div class="wrapper">
        <div class="box box1">Content of box 1</div>
        <div class="box box2">Content of box 2</div>
        <div class="box box3">Content of box 3</div>
        <div class="box box4">Content of box 4</div>
        <div class="box box5">Content of box 5</div>
        <div class="box box6">Content of box 6</div>
        <div class="box box7">Content of box 7</div>
    </div>
</body>
</html>
body {
    margin: 0;
    display: flex;
    justify-content: center;
}

.wrapper {
  column-count: auto;
  column-width: 320px; /* 300px + 20px gap */
  column-gap: 20px;
  width: 100%;
  max-width: 1000px;
  padding: 20px;
  box-sizing: border-box;
}

.box {
  background-color: lightblue;
  border: 1px solid blue;
  padding: 20px;
  font-size: 1.5em;
  text-align: center;
  width: 300px;
  box-sizing: border-box;
  display: inline-block;
  margin-bottom: 20px;
  break-inside: avoid-column;
}

.box1 {
  height: 800px;
}
.box2 {
  height: 300px;
}
.box3 {
  height: 300px;
}
.box4 {
  height: 300px;
}

enter image description here

Lookeron answered 19/4, 2023 at 10:0 Comment(1)
I followed your advice to switch from display:grid to your solution. However, as in your illustration, the order is on a column-basis instead of a row-basis. Instead of 1, 4 on the first row, is there no way to have it go 1,2,3 etc?Papacy
S
0

For people curious about a Tailwind solution, it's pretty straightforward. If you had a grid in place to place the different elements, remove it and put that instead:

<div class="columns-1 lg:columns-3 lg:gap-16">
  <div class="inline-block mb-3">
    MY CONTENT
  </div>
  <div class="inline-block mb-3">
    MY CONTENT
  </div>
  <div class="inline-block mb-3">
    MY CONTENT
  </div>
  <div class="inline-block mb-3">
    MY CONTENT
  </div>
  <div class="inline-block mb-3">
    MY CONTENT
  </div>
  <div class="inline-block mb-3">
    MY CONTENT
  </div>
</div>

This is an example with a 3-column setup and is responsive.

Spendthrift answered 4/4 at 10:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.