原文信息
- 文章地址:Making a Masonry Layout That Works Today
- 发布时间:2025/07/28
- 更新时间:2025/11/25
声明
翻译开工时,原作更新时间为 2025/11/25。
封面用了原文的。
代码 100% 还原,原文代码示例用 codepen。
翻译了部分注释、<img> 的
alt。译文与原文略有出入。
去年,许多 CSS 专家对新的砌砖特性,深入探讨过可能采用的语法形式。主要分成两派,还有一派介于两者之间:
- Use
display: masonry - Use
grid-template-rows: masonry - Use
item-pack: collapse
Firefox 已经支持用第二种语法去实现了,Chrome 还在测试第一种语法。有原生支持是好事,可如果浏览器不齐心,就是支持不彻底,等于彻底不支持……
看他们商量不出长短,就自己研究。在众多浏览器都能用的法子让我研究出来了!只要 66 行 JavaScript 就实现了!
体验一下这个示例,证明我所言非虚。如果你的首屏要用这种砌砖网格,建议去掉图片,改善用户体验。因为设计成了先等待媒体加载,所以会有轻微延迟。
做了什么?
别看 JavaScript 只有 66 行,简约而不简单:
- 列数可自定义。
- 格子可跨多列。
- 等媒体加载完,再计算格子的尺寸。
ResizeObserver监听变化,实现响应式。
这些特性足够可靠,生产环境放心用,而且比别人用 Flexbox 仿制的砌砖更灵活。
技巧分享。 这一套搭配 Tailwind 的响应式变体和任意值,砌砖网格能更灵活,不用再多余写 CSS。
回归正题,看它是怎么运作的。
先从 polyfill 开始
在 Firefox 创建砌砖的写法。
.masonry {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--item-width, 200px), 100%), 1fr)
);
grid-template-rows: masonry;
grid-auto-flow: dense; /* 写不写都行, 建议加上 */
}
检查是否支持砌砖,最佳方案是看能不能让 grid-template-rows 值为 masonry。
function isMasonrySupported(container) {
return getComputedStyle(container).gridTemplateRows === 'masonry'
}
如果已经原生支持,就甭忙活了。
const containers = document.querySelectorAll('.masonry')
containers.forEach(async container => {
if (isMasonrySupported(container)) return
})
再让砌砖变简单
我声明一下,我不是首创。
当时我在网上搜,如何实现砌砖网格布局,偶然发现了这个技巧。我得给那位开创者颁奖——算了,颁给我也一样,我这还学以致用,做成了一套呢。
原理如下:
- 把
grid-auto-rows设为0px。 - 再把
row-gap设为1px。 - 然后用
getBoundingClientRect获取网格高度。 - 最后,
height加上column-gap算出每个格子该占几行。
如果按以往用 CSS Grid 的思路,确实会反直觉。咱们一步一步来,到后面就全明白了!
一步一步来
首先,每个格子的 grid-auto-rows 设为 0px,CSS Grid 仍然保持排列顺序!
containers.forEach(async container => {
// ...
container.style.gridAutoRows = '0px'
})
其次,row-gap 设为 1px。这时候出现逐行堆叠的现象,每一行都比前一行低 1px。
containers.forEach(async container => {
// ...
container.style.gridAutoRows = '0px'
+ container.style.setProperty('row-gap', '1px', 'important')
})
再次,假设网格项里没有媒体元素,用 getBoundingClientRect 直接就获取到每个网格项的高度。
接着,用 height 数值去设置 grow-row-end,使网格项的「高度」恢复。之所以成功,是因为现在 row-gap 为 1px。
现在,网格项基本归位:
containers.forEach(async container => {
// ...
let items = container.children
layout({ items })
})
function layout({ items }) {
items.forEach(item => {
const ib = item.getBoundingClientRect()
item.style.gridRowEnd = `span ${Math.round(ib.height)}`
})
}
现在恢复网格项之间的行间距。砌砖网格的 column-gap 和 row-gap 通常都一样,可以读 column-gap 获取行间距。
读到值后,加到 grid-row-end,就能撑开这个格子在网格占的行数(即「高度」)。
一旦获取到该值,我们将其加到 grid-row-end 上,以扩展项目在网格中所占据的行数。
containers.forEach(async container => {
// ...
const items = container.children
const colGap = parseFloat(getComputedStyle(container).columnGap)
layout({ items, colGap })
})
function layout({ items, colGap }) {
items.forEach(item => {
const ib = item.getBoundingClientRect()
item.style.gridRowEnd = `span ${Math.round(ib.height + colGap)}`
})
}
搞定!之后,就是为生产环境做准备了。
等待媒体加载
如果随便给一个格子加图片,布局就被打乱了。「问题」在于格子高度。
之所以出错,是因为我们在图片加载前就获取高度。这时候 DOM 不知图片尺寸。要修复,就得在 layout 调用之前,等待媒体加载完成。
用下面这段代码就能搞定(具体我就不解释了哈,这跟 CSS 技巧关系不大 😅):
containers.forEach(async container => {
// ...
try {
await Promise.all([areImagesLoaded(container), areVideosLoaded(container)])
} catch(e) {}
// Run the layout function after images are loaded
layout({ items, colGap })
})
// 检查图片是否已加载
async function areImagesLoaded(container) {
const images = Array.from(container.querySelectorAll('img'))
const promises = images.map(img => {
return new Promise((resolve, reject) => {
if (img.complete) return resolve()
img.onload = resolve
img.onerror = reject
})
})
return Promise.all(promises)
}
// 检查视频是否已加载
function areVideosLoaded(container) {
const videos = Array.from(container.querySelectorAll('video'))
const promises = videos.map(video => {
return new Promise((resolve, reject) => {
if (video.readyState === 4) return resolve()
video.onloadedmetadata = resolve
video.onerror = reject
})
})
return Promise.all(promises)
}
好了!现在,CSS 砌砖网格能处理媒体了。
具备响应式
这个简单。只需要 ResizeObserver API 监听。
一旦网格容器尺寸变化,就调用 layout 函数:
containers.forEach(async container => {
// ...
const observer = new ResizeObserver(observerFn)
observer.observe(container)
function observerFn(entries) {
for (const entry of entries) {
layout({colGap, items})
}
}
})
演示采用标准 Resize Observer API。我推荐我前几天封装好的 resizeObserver 函数,更简洁。
containers.forEach(async container => {
// ...
const observer = resizeObserver(container, {
callback () {
layout({colGap, items})
}
})
})
完工!祝贺你得到一个功能强大的砌砖网格!
怎么样,是不是很简单!
Splendid Labz 砌砖网格
可以试试我做的 Splendid Labz 版本砌砖,如果你能接受外来的代码。
只需要安装辅助库,再添加必要代码:
# 安装库
npm install @splendidlabz/styles
/* 导入所有 layout 代码 */
@import '@splendidlabz/styles/layouts'
// 使用砌砖脚本
import { masonry } from '@splendidlabz/styles/scripts'
masonry()
为了网页开发更轻松,我一直在开发大量工具,包括这个,都归到了 Splendid Labz 这个品牌。
如果你喜欢,那你可能会对其他一些简化布局构建过程的工具感兴趣。
好了,希望你喜欢这篇文章,也祝你尝试砌砖网格一切顺利。