search
HomeWeb Front-endCSS TutorialA Lightweight Masonry Solution

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/imghwm/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174364597525146.jpg?x-oss-process=image/resize,p_40" class="lazy" alt="A Lightweight Masonry Solution">

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

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

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

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

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

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!');
}

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`));
}

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

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`;
    });
  }
}

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/imghwm/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174364597550777.jpg?x-oss-process=image/resize,p_40" class="lazy" alt="A Lightweight Masonry Solution">
</a>

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*/
}

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*/
}

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

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);

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

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

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

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
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
Next Level CSS Styling for CursorsNext Level CSS Styling for CursorsApr 23, 2025 am 11:04 AM

Custom cursors with CSS are great, but we can take things to the next level with JavaScript. Using JavaScript, we can transition between cursor states, place dynamic text within the cursor, apply complex animations, and apply filters.

Worlds Collide: Keyframe Collision Detection Using Style QueriesWorlds Collide: Keyframe Collision Detection Using Style QueriesApr 23, 2025 am 10:42 AM

Interactive CSS animations with elements ricocheting off each other seem more plausible in 2025. While it’s unnecessary to implement Pong in CSS, the increasing flexibility and power of CSS reinforce Lee's suspicion that one day it will be a

Using CSS backdrop-filter for UI EffectsUsing CSS backdrop-filter for UI EffectsApr 23, 2025 am 10:20 AM

Tips and tricks on utilizing the CSS backdrop-filter property to style user interfaces. You’ll learn how to layer backdrop filters among multiple elements, and integrate them with other CSS graphical effects to create elaborate designs.

SMIL on?SMIL on?Apr 23, 2025 am 09:57 AM

Well, it turns out that SVG's built-in animation features were never deprecated as planned. Sure, CSS and JavaScript are more than capable of carrying the load, but it's good to know that SMIL is not dead in the water as previously

'Pretty' is in the eye of the beholder'Pretty' is in the eye of the beholderApr 23, 2025 am 09:40 AM

Yay, let's jump for text-wrap: pretty landing in Safari Technology Preview! But beware that it's different from how it works in Chromium browsers.

CSS-Tricks Chronicles XLIIICSS-Tricks Chronicles XLIIIApr 23, 2025 am 09:35 AM

This CSS-Tricks update highlights significant progress in the Almanac, recent podcast appearances, a new CSS counters guide, and the addition of several new authors contributing valuable content.

Tailwind's @apply Feature is Better Than it SoundsTailwind's @apply Feature is Better Than it SoundsApr 23, 2025 am 09:23 AM

Most of the time, people showcase Tailwind's @apply feature with one of Tailwind's single-property utilities (which changes a single CSS declaration). When showcased this way, @apply doesn't sound promising at all. So obvio

Feeling Like I Have No Release: A Journey Towards Sane DeploymentsFeeling Like I Have No Release: A Journey Towards Sane DeploymentsApr 23, 2025 am 09:19 AM

Deploying like an idiot comes down to a mismatch between the tools you use to deploy and the reward in complexity reduced versus complexity added.

See all articles

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

PhpStorm Mac version

PhpStorm Mac version

The latest (2018.2.1) professional PHP integrated development tool

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Linux new version

SublimeText3 Linux new version

SublimeText3 Linux latest version

mPDF

mPDF

mPDF is a PHP library that can generate PDF files from UTF-8 encoded HTML. The original author, Ian Back, wrote mPDF to output PDF files "on the fly" from his website and handle different languages. It is slower than original scripts like HTML2FPDF and produces larger files when using Unicode fonts, but supports CSS styles etc. and has a lot of enhancements. Supports almost all languages, including RTL (Arabic and Hebrew) and CJK (Chinese, Japanese and Korean). Supports nested block-level elements (such as P, DIV),

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Integrate Eclipse with SAP NetWeaver application server.