Table of Contents
First, enable flags
Let's start with some markups
Now, let's apply some styles
JavaScript fallback solution
Some minor improvements
A more realistic structure
Make the first element span the mesh
Handle grid items with variable aspect ratios
But, however, but...
How is browser support?
What about the situation without JavaScript?
Home Web Front-end CSS Tutorial A Lightweight Masonry Solution

A Lightweight Masonry Solution

Apr 03, 2025 am 10:06 AM

A Lightweight Masonry Solution

In May, I learned that Firefox added masonry layout functionality in CSS Grid. Masonry layout is something I have always wanted to implement from scratch, but I have never known where to start. So I naturally looked at the demo and then I had a flash of inspiration when I understood how this new CSS feature works.

Currently, support is limited to Firefox (and, even there, only if specific flags are enabled), but this still gives me a starting point for a JavaScript implementation that can cover browsers currently lacking support.

Firefox implements masonry layout in CSS by setting grid-template-rows (as shown in the example) or grid-template-columns to masonry value.

My approach is to leverage this feature to support the browser (again, it only refers to Firefox for now) and create a JavaScript fallback scheme for the rest of the browsers. Let's see how to achieve this using a specific case of an image grid.

First, enable flags

To do this, we visit about:config in Firefox and search for "masonry". This will display the layout.css.grid-template-masonry-value.enabled flag, which we can enable by double-clicking to change its value from false (the default value) to true .

Let's start with some markups

The HTML structure is as follows:

<img src="/static/imghw/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174364597525146.jpg" class="lazy" alt="A Lightweight Masonry Solution">
Copy after login

Now, let's apply some styles

First, we set the top level element as a CSS grid container. Next, we define the maximum width for the image, for example 10em . We also want these images to shrink to any space available to the grid content box, and if the viewport becomes too narrow to accommodate a single 10em column grid, the value actually set is Min(10em, 100%) . Since responsive design is very important nowadays, instead of using a fixed number of columns, we automatically adapt as many columns of this width as possible:

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

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

  > * { width: $w; }
}
Copy after login

Note that we used Min() instead of min() to avoid Sass conflicts.

OK, this is a grid!

It's not a very nice grid, though, so let's force its contents to be centered horizontally and then add the grid gap and fill, both equal to a spacing value ( $s ). We also set up a background for easier viewing.

 $s: .5em;

/* Masonry grid style*/
.grid--masonry {
  /* Same as previous style*/
  justify-content: center;
  grid-gap: $s;
  padding: $s;
}

/* Beautification style*/
html { background: #555; }
Copy after login

After a little beautification of the grid, we turned to doing the same for the grid project (i.e., the image). Let's apply a filter to make them look a little more uniform while adding some extra style with slightly rounded corners and shadows.

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

Now, for browsers that support masonry layouts, we just need to declare it:

 .grid--masonry {
  /* Same as previous style*/
  grid-template-rows: massive;
}
Copy after login

While this doesn't work in most browsers, in Firefox with the flags explained earlier it produces the expected results.

But what about other browsers? This is what we need...

JavaScript fallback solution

In order to save the JavaScript code that the browser must run, we first check whether there is any .grid--masonry element on the page, and whether the browser has understood and applied masonry value of grid-template-rows . Note that this is a common approach, assuming that there may be multiple such meshes on our page.

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

if (grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  console.log('Oops, masonry layout does not support?');
} else {
  console.log('Too good, no operation required!');
}
Copy after login

If the new masonry feature is not supported, then we will get the row gap and mesh items for each masonry mesh and then set the number of columns (each mesh is initially 0).

 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 item: ${grid.items.length}; grid gap: ${grid.gap}px`));
}
Copy after login

Note that we need to make sure that the child nodes are element nodes (this means that their nodeType is 1). Otherwise, we may end up with text nodes composed of carriage returns in the project array.

Before proceeding, we have to make sure that the page is loaded and the elements are still moving. Once we have dealt with this problem, we take each grid and read its current number of columns. If this is different from the value we already have, then we will update the old value and rearrange the grid items.

 if (grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
  grids = grids.map(/* is the same as before*/);

  function layout() {
    grids.forEach(grid => {
      /* Get the resize/loaded column number*/
      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);
}
Copy after login

Note that calling layout() function is an operation we need to perform when both initial loading and resizing.

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

If the viewport is narrow enough and we only have one column, then we are done!

Otherwise, we will skip the first ncol items and loop through the rest. For each item considered, we calculate the bottom edge position of the above item and its current position of the top edge. This allows us to calculate how much vertical movement is needed so that its top edge is located in a grid gap below the bottom edge of the project above.

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

  /* Restore initial positioning, no border*/
  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 the above project*/
          curr_ini = c.getBoundingClientRect().top; /* top edge of the current project*/

      c.style.marginTop = `${prev_fin grid.gap - curr_ini}px`;
    });
  }
}
Copy after login

Now we have a working cross-browser solution!

Some minor improvements

A more realistic structure

In the real world, we are more likely to wrap each image in a link to its full-size image so that the large image can be opened in the light box (or we navigate it as a fallback).

<a href="https://www.php.cn/link/849c1f472f609bb4a3bacafef177f541">
  <img src="/static/imghw/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174364597550777.jpg" class="lazy" alt="A Lightweight Masonry Solution">
</a>
Copy after login

This means we need to change the CSS a little bit, too. While we no longer need to explicitly set the width of the grid items - because they are now links - we need to set align-self: start , because unlike images, they by default stretch to cover the entire row height, which disturbs our algorithm.

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

img {
  display: block; /* Avoid strange extra space at the bottom*/
  width: 100%;
  /* Same as previous style*/
}
Copy after login

Make the first element span the mesh

We can also make the first project horizontally span the entire mesh (which means we should probably also limit its height and make sure the image does not overflow or deform):

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

img {
  max-height: inherit;
  object-fit: cover;
  /* Same as previous style*/
}
Copy after login

We also need to add another filter when getting the list of grid items to exclude this stretched item:

 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
}));
Copy after login

Handle grid items with variable aspect ratios

Suppose we want to use this solution for purposes like blogging. We keep the exact same JS and almost identical masonry-specific CSS - we only change the maximum width of the column and remove the max-height limit for the first item.

As can be seen from the demonstration below, our solution works perfectly in this case, and we have a blog post grid:

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

However, if we want the column width to have some flexibility, for example:

 $w: minmax(Min(20em, 100%), 1fr);
Copy after login

Then we will encounter problems when resizing:

The combination of the change in grid items' width and the fact that each item's text content is different means that when a certain threshold is exceeded, we may get different number of text lines for grid items (and thus change the height), but other items do not. If the number of columns has not changed, the vertical offset is not recalculated and we end up with overlap or larger gaps.

To solve this problem, we need to recalculate the offset when the current grid height of at least one item changes. This means we also need to test whether there are more than zero items in the current grid that have changed their height. Then we need to reset this value at the end of if block so that we don't have to unnecessarily rearrange the items next time.

 if (grid.ncol !== ncol || grid.mod) {
  /* Same as before*/
  grid.mod = 0;
}
Copy after login

OK, but how do we change this grid.mod value? My first idea is to use 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);
}
Copy after login

This does make it possible to rearrange the grid items if necessary, even if the number of grid columns has not changed. But it also makes it meaningless even if conditions are not available!

This is because it changes grid.mod to 1 when the height or width of at least one item changes. The height of the project will change due to text reflow, which is caused by width change. However, the width change will happen every time we adjust the viewport size, and it does not necessarily trigger the height change.

That's why I ended up deciding to store previous project heights and check if they have changed when resized to determine if grid.mod remains at 0:

 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*/
  });
}
Copy after login

That's it! Now we have a nice lightweight solution. Compressed JavaScript is less than 800 bytes, while strict masonry-related styles are less than 300 bytes.

But, however, but...

How is browser support?

Well, @supports happens to have better browser support than any newer CSS feature used here, so we can put the good stuff in it and provide a basic non-masonry mesh for unsupported browsers. This version is backward compatible to IE9.

It may look different, but it looks good and has a perfect function. Supporting a browser does not mean copying all visual effects for it. This means the page works and doesn't look damaged or ugly.

What about the situation without JavaScript?

Well, we can only apply fancy styles if the root element has js class we added via JavaScript! Otherwise, we will get a base grid of the same size for all items.

The above is the detailed content of A Lightweight Masonry Solution. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

It&#039;s out! Congrats to the Vue team for getting it done, I know it was a massive effort and a long time coming. All new docs, as well.

Can you get valid CSS property values from the browser? Can you get valid CSS property values from the browser? Apr 02, 2025 pm 06:17 PM

I had someone write in with this very legit question. Lea just blogged about how you can get valid CSS properties themselves from the browser. That&#039;s like this.

A bit on ci/cd A bit on ci/cd Apr 02, 2025 pm 06:21 PM

I&#039;d say "website" fits better than "mobile app" but I like this framing from Max Lynch:

Stacked Cards with Sticky Positioning and a Dash of Sass Stacked Cards with Sticky Positioning and a Dash of Sass Apr 03, 2025 am 10:30 AM

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

Using Markdown and Localization in the WordPress Block Editor Using Markdown and Localization in the WordPress Block Editor Apr 02, 2025 am 04:27 AM

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

Comparing Browsers for Responsive Design Comparing Browsers for Responsive Design Apr 02, 2025 pm 06:25 PM

There are a number of these desktop apps where the goal is showing your site at different dimensions all at the same time. So you can, for example, be writing

How to Use CSS Grid for Sticky Headers and Footers How to Use CSS Grid for Sticky Headers and Footers Apr 02, 2025 pm 06:29 PM

CSS Grid is a collection of properties designed to make layout easier than it’s ever been. Like anything, there&#039;s a bit of a learning curve, but Grid is

Google Fonts   Variable Fonts Google Fonts Variable Fonts Apr 09, 2025 am 10:42 AM

I see Google Fonts rolled out a new design (Tweet). Compared to the last big redesign, this feels much more iterative. I can barely tell the difference

See all articles