惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
L
LINUX DO - 热门话题
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
爱范儿
爱范儿
D
DataBreaches.Net
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
S
SegmentFault 最新的问题
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
P
Proofpoint News Feed
The Hacker News
The Hacker News
T
ThreatConnect
N
News and Events Feed by Topic
T
Threatpost
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - Franky
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
F
Future of Privacy Forum
F
Full Disclosure
Cyberwarzone
Cyberwarzone
J
Java Code Geeks
李成银的技术随笔
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
博客园_首页
Scott Helme
Scott Helme
Google DeepMind News
Google DeepMind News
美团技术团队
Malwarebytes
Malwarebytes
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
Recent Announcements
Recent Announcements
C
CXSECURITY Database RSS Feed - CXSecurity.com

CSS-Tricks

Revealing Text With CSS letter-spacing | CSS-Tricks Technical Writing in the AI Age | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks The State of CSS Centering in 2026 | CSS-Tricks Stack Overflow: When We Stop Asking | CSS-Tricks Cross-Document View Transitions: The Gotchas Nobody Mentions | CSS-Tricks What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More | CSS-Tricks Computing and Displaying Discounted Prices in CSS | CSS-Tricks rotateX() | CSS-Tricks rotateY() | CSS-Tricks rotateZ() | CSS-Tricks rotate() | CSS-Tricks Soon We Can Finally Banish JavaScript to the ShadowRealm | CSS-Tricks Using CSS corner-shape For Folded Corners | CSS-Tricks A Scrollytelling Gift for Mum on Mother’s Day 2026 | CSS-Tricks Google’s Prompt API | CSS-Tricks Making Zigzag CSS Layouts With a Grid + Transform Trick | CSS-Tricks Fixed-Height Cards: More Fragile Than They Look | CSS-Tricks What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More | CSS-Tricks The Importance of Native Randomness in CSS | CSS-Tricks contrast() | CSS-Tricks contrast-color() | CSS-Tricks Let’s Use the Nonexistent ::nth-letter Selector Now | CSS-Tricks Quick Hit #126 Recreating Apple’s Vision Pro Animation in CSS | CSS-Tricks Quick Hit #125 Enhancing Astro With a Markdown Component | CSS-Tricks Quick Hit #124 Markdown + Astro = ❤️ | CSS-Tricks Quick Hit #123 What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More | CSS-Tricks A Well-Designed JavaScript Module System is Your First Architecture Decision | CSS-Tricks hypot() | CSS-Tricks The Radio State Machine | CSS-Tricks 7 View Transitions Recipes to Try | CSS-Tricks Quick Hit #122 Quick Hit #121 Selecting a Date Range in CSS | CSS-Tricks saturate() | CSS-Tricks justify-self | CSS-Tricks Quick Hit #120 Alternatives to the !important Keyword | CSS-Tricks Quick Hit #119 New CSS Multi-Column Layout Features in Chrome | CSS-Tricks Quick Hit #118 Making Complex CSS Shapes Using shape() | CSS-Tricks Quick Hit #117 Front-End Fools: Top 10 April Fools’ UI Pranks of All Time | CSS-Tricks Sniffing Out the CSS Olfactive API | CSS-Tricks What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More | CSS-Tricks Quick Hit #116 Form Automation Tips for Happier User and Clients | CSS-Tricks Quick Hit #115 Generative UI Notes | CSS-Tricks Quick Hit #114 Quick Hit #113 Experimenting With Scroll-Driven corner-shape Animations | CSS-Tricks Quick Hit #112 JavaScript for Everyone: Destructuring | CSS-Tricks Quick Hit #111 Quick Hit #110 What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More | CSS-Tricks 4 Reasons That Make Tailwind Great for Building Layouts | CSS-Tricks Quick Hit #109 Quick Hit #108 Abusing Customizable Selects | CSS-Tricks Quick Hit #107 The Value of z-index | CSS-Tricks Quick Hit #106 The Different Ways to Select <html> in CSS Quick Hit #105 Popover API or Dialog API: Which to Choose? Quick Hit #104 What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More Yet Another Way to Center an (Absolute) Element An Exploit ... in CSS?! Quick Hit #103 A Complete Guide to Bookmarklets Quick Hit #102 Loading Smarter: SVG vs. Raster Loaders in Modern Web Design Potentially Coming to a Browser :near() You Quick Hit #101 Distinguishing "Components" and "Utilities" in Tailwind Quick Hit #100 Spiral Scrollytelling in CSS With sibling-index() Interop 2026 Quick Hit #99 What’s !important #5: Lazy-loading iframes, Repeating corner-shape Backgrounds, and More Quick Hit #98 Making a Responsive Pyramidal Grid With Modern CSS Approximating contrast-color() With Other CSS Features Quick Hit #97 Trying to Make the Perfect Pie Chart in CSS Quick Hit #96 Quick Hit #95 CSS Bar Charts Using Modern Functions Quick Hit #94 No Hassle Visual Code Theming: Publishing an Extension Quick Hit #93
A Lightweight Masonry Solution
CSS-Tricks · 2020-08-03 · via CSS-Tricks

Back in May, I learned about Firefox adding masonry to CSS grid. Masonry layouts are something I’ve been wanting to do on my own from scratch for a very long time, but have never known where to start. So, naturally, I checked the demo and then I had a lightbulb moment when I understood how this new proposed CSS feature works.

Support is obviously limited to Firefox for now (and, even there, only behind a flag), but it still offered me enough of a starting point for a JavaScript implementation that would cover browsers that currently lack support.

The way Firefox implements masonry in CSS is by setting either grid-template-rows (as in the example) or grid-template-columns to a value of masonry.

My approach was to use this for supporting browsers (which, again, means just Firefox for now) and create a JavaScript fallback for the rest. Let’s look at how this works using the particular case of an image grid.

First, enable the flag

In order to do this, we go to about:config in Firefox and search for “masonry.” This brings up the layout.css.grid-template-masonry-value.enabled flag, which we enable by double clicking its value from false (the default) to true.

Screenshot showing the masonry flag being enabled according to the instructions above.
Making sure we can test this feature.

Let’s start with some markup

The HTML structure looks something like this:

<section class="grid--masonry">
  <img src="black_cat.jpg" alt="black cat" />
  <!-- more such images following -->
</section>

Now, let’s apply some styles

The first thing we do is make the top-level element a CSS grid container. Next, we define a maximum width for our images, let’s say 10em. We also want these images to shrink to whatever space is available for the grid’s content-box if the viewport becomes too narrow to accommodate for a single 10em column grid, so the value we actually set is Min(10em, 100%). Since responsivity is important these days, we don’t bother with a fixed number of columns, but instead auto-fit as many columns of this width as we can:

$w: Min(10em, 100%);

.grid--masonry {
  display: grid;
  grid-template-columns: repeat(auto-fit, $w);
	
  > * { width: $w; }
}

Note that we’ve used Min() and not min() in order to avoid a Sass conflict.

Well, that’s a grid!

Not a very pretty one though, so let’s force its content to be in the middle horizontally, then add a grid-gap and padding that are both equal to a spacing value ($s). We also set a background to make it easier on the eyes.

$s: .5em;

/* masonry grid styles */
.grid--masonry {
  /* same styles as before */
  justify-content: center;
  grid-gap: $s;
  padding: $s
}

/* prettifying styles */
html { background: #555 }

Having prettified the grid a bit, we turn to doing the same for the grid items, which are the images. Let’s apply a filter so they all look a bit more uniform, while giving a little additional flair with slightly rounded corners and a box-shadow.

img {
  border-radius: 4px;
  box-shadow: 2px 2px 5px rgba(#000, .7);
  filter: sepia(1);
}

The only thing we need to do now for browsers that support masonry is to declare it:

.grid--masonry {
  /* same styles as before */
  grid-template-rows: masonry;
}

While this won’t work in most browsers, it produces the desired result in Firefox with the flag enabled as explained earlier.

Screenshot showing the masonry result in Firefox alongside DevTools where we can see what's under the hood.
grid-template-rows: masonry working in Firefox with the flag enabled (Demo).

But what about the other browsers? That’s where we need a…

JavaScript fallback

In order to be economical with the JavaScript the browser has to run, we first check if there are any .grid--masonry elements on that page and whether the browser has understood and applied the masonry value for grid-template-rows. Note that this is a generic approach that assumes we may have multiple such grids on a page.

let grids = [...document.querySelectorAll('.grid--masonry')];

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  console.log('boo, masonry not supported 😭')
}
else console.log('yay, do nothing!')
Screenshot showing how Firefox with the flag enabled as explained above logs 'yay, do nothing!', while other browsers log 'boo, masonry not supported'.
Support test (live).

If the new masonry feature is not supported, we then get the row-gap and the grid items for every masonry grid, then set a number of columns (which is initially 0 for each grid).

let grids = [...document.querySelectorAll('.grid--masonry')];

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  grids = grids.map(grid => ({
    _el: grid, 
    gap: parseFloat(getComputedStyle(grid).gridRowGap), 
    items: [...grid.childNodes].filter(c => c.nodeType === 1), 
    ncol: 0
  }));
  
  grids.forEach(grid => console.log(`grid items: ${grid.items.length}; grid gap: ${grid.gap}px`))
}

Note that we need to make sure the child nodes are element nodes (which means they have a nodeType of 1). Otherwise, we can end up with text nodes consisting of carriage returns in the array of items.

Screenshot showing the number of items and the row-gap logged in the console.
Checking we got the correct number of items and gap (live).

Before proceeding further, we have to ensure the page has loaded and the elements aren’t still moving around. Once we’ve handled that, we take each grid and read its current number of columns. If this is different from the value we already have, then we update the old value and rearrange the grid items.

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  grids = grids.map(/* same as before */);
	
  function layout() {
    grids.forEach(grid => {
      /* get the post-resize/ load number of columns */
      let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length;

      if(grid.ncol !== ncol) {
        grid.ncol = ncol;
        console.log('rearrange grid items')
      }
    });
  }
	
  addEventListener('load', e => {		
    layout(); /* initial load */
    addEventListener('resize', layout, false)
  }, false);
}

Note that calling the layout() function is something we need to do both on the initial load and on resize.

Screenshot showing the message we get when relayout is necessry.
When we need to rearrange grid items (live).

To rearrange the grid items, the first step is to remove the top margin on all of them (this may have been set to a non-zero value to achieve the masonry effect before the current resize).

If the viewport is narrow enough that we only have one column, we’re done!

Otherwise, we skip the first ncol items and we loop through the rest. For each item considered, we compute the position of the bottom edge of the item above and the current position of its top edge. This allows us to compute how much we need to move it vertically such that its top edge is one grid gap below the bottom edge of the item above.

/* if the number of columns has changed */
if(grid.ncol !== ncol) {
  /* update number of columns */
  grid.ncol = ncol;

  /* revert to initial positioning, no margin */
  grid.items.forEach(c => c.style.removeProperty('margin-top'));

  /* if we have more than one column */
  if(grid.ncol > 1) {
    grid.items.slice(ncol).forEach((c, i) => {
      let prev_fin = grid.items[i].getBoundingClientRect().bottom /* bottom edge of item above */, 
          curr_ini = c.getBoundingClientRect().top /* top edge of current item */;
						
      c.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`
    })
  }
}

We now have a working, cross-browser solution!

A couple of minor improvements

A more realistic structure

In a real world scenario, we’re more likely to have each image wrapped in a link to its full size so that the big image opens in a lightbox (or we navigate to it as a fallback).

<section class='grid--masonry'>
  <a href='black_cat_large.jpg'>
    <img src='black_cat_small.jpg' alt='black cat'/>
  </a>
  <!-- and so on, more thumbnails following the first -->
</section>

This means we also need to alter the CSS a bit. While we don’t need to explicitly set a width on the grid items anymore — as they’re now links — we do need to set align-self: start on them because, unlike images, they stretch to cover the entire row height by default, which will throw off our algorithm.

.grid--masonry > * { align-self: start; }

img {
  display: block; /* avoid weird extra space at the bottom */
  width: 100%;
  /* same styles as before */
}

Making the first element stretch across the grid

We can also make the first item stretch horizontally across the entire grid (which means we should probably also limit its height and make sure the image doesn’t overflow or get distorted):

.grid--masonry > :first-child {
  grid-column: 1/ -1;
  max-height: 29vh;
}

img {
  max-height: inherit;
  object-fit: cover;
  /* same styles as before */
}

We also need to exclude this stretched item by adding another filter criterion when we get the list of grid items:

grids = grids.map(grid => ({
  _el: grid, 
  gap: parseFloat(getComputedStyle(grid).gridRowGap), 
  items: [...grid.childNodes].filter(c => 
    c.nodeType === 1 && 
    +getComputedStyle(c).gridColumnEnd !== -1
  ), 
  ncol: 0
}));

Handling grid items with variable aspect ratios

Let’s say we want to use this solution for something like a blog. We keep the exact same JS and almost the exact same masonry-specific CSS – we only change the maximum width a column may have and drop the max-height restriction for the first item.

As it can be seen from the demo below, our solution also works perfectly in this case where we have a grid of blog posts:

You can also resize the viewport to see how it behaves in this case.

However, if we want the width of the columns to be somewhat flexible, for example, something like this:

$w: minmax(Min(20em, 100%), 1fr)

Then we have a problem on resize:

The changing width of the grid items combined with the fact that the text content is different for each means that when a certain threshold is crossed, we may get a different number of text lines for a grid item (thus changing the height), but not for the others. And if the number of columns doesn’t change, then the vertical offsets don’t get recomputed and we end up with either overlaps or bigger gaps.

In order to fix this, we need to also recompute the offsets whenever at least one item’s height changes for the current grid. This means we need to also need to test if more than zero items of the current grid have changed their height. And then we need to reset this value at the end of the if block so that we don’t rearrange the items needlessly next time around.

if(grid.ncol !== ncol || grid.mod) {
  /* same as before */
  grid.mod = 0
}

Alright, but how do we change this grid.mod value? My first idea was to use a ResizeObserver:

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  let o = new ResizeObserver(entries => {
    entries.forEach(entry => {
      grids.find(grid => grid._el === entry.target.parentElement).mod = 1
    });
  });
  
  /* same as before */
  
  addEventListener('load', e => {
    /* same as before */
    grids.forEach(grid => { grid.items.forEach(c => o.observe(c)) })
  }, false)
}

This does the job of rearranging the grid items when necessary even if the number of grid columns doesn’t change. But it also makes even having that if condition pointless!

This is because it changes grid.mod to 1 whenever the height or the width of at least one item changes. The height of an item changes due to the text reflow, caused by the width changing. But the change in width happens every time we resize the viewport and doesn’t necessarily trigger a change in height.

This is why I eventually decided on storing the previous item heights and checking whether they have changed on resize to determine whether grid.mod remains 0 or not:

function layout() {
  grids.forEach(grid => {
    grid.items.forEach(c => {
      let new_h = c.getBoundingClientRect().height;
				
      if(new_h !== +c.dataset.h) {
        c.dataset.h = new_h;
        grid.mod++
      }
    });
			
    /* same as before */
  })
}

That’s it! We now have a nice lightweight solution. The minified JavaScript is under 800 bytes, while the strictly masonry-related styles are under 300 bytes.

But, but, but…

What about browser support?

Well, @supports just so happens to have better browser support than any of the newer CSS features used here, so we can put the nice stuff inside it and have a basic, non-masonry grid for non-supporting browsers. This version works all the way back to IE9.

Screenshot showing the IE grid.
The result in Internet Explorer

It may not look the same, but it looks decent and it’s perfectly functional. Supporting a browser doesn’t mean replicating all the visual candy for it. It means the page works and doesn’t look broken or horrible.

What about the no JavaScript case?

Well, we can apply the fancy styles only if the root element has a js class which we add via JavaScript! Otherwise, we get a basic grid where all the items have the same size.

Screenshot showing the no JS grid.
The no JavaScript result (Demo).