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!

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.

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

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.

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

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.

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.

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

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.


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

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

Hot Article

Hot Tools

PhpStorm Mac version
The latest (2018.2.1) professional PHP integrated development tool

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Linux new version
SublimeText3 Linux latest version

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
Integrate Eclipse with SAP NetWeaver application server.
