等了好久终于等到今天,瀑布流布局到底怎么搞,CSS 终于给答案了

4,515字
19–29 分钟
in

搞定网页上的瀑布流布局,这一路走来踩过的坑简直能填满一个马里亚纳海沟。

目录

摘要

从2017年开始,CSS圈子里就在吵一个事儿:怎么用纯CSS搞定像Pinterest那种错落有致的瀑布流布局。用过的人都知道,以前想实现这效果,要么用multi-col让内容从上往下跑,要么靠flexbox各种骚操作,或者直接上JS库。中间有人提议用griddense属性硬塞,但需要提前知道每个元素的高度,对动态内容完全不友好。就这么折腾了快八年,CSS工作组终于在2025年一锤定音,决定复用grid的模板和定位属性,用一个全新的display: grid-lanes来开启瀑布流模式。以后写布局,终于不用再跟float、绝对定位这些老古董死磕了。

瀑布流布局的前世今生

那些年用过的歪门邪道

回想当年做这种错落排版的页面,大家手里能用的家伙什儿就那么几样。multi-col算是官方给的一个方向,但它的毛病是内容会老老实实按列填充,第一列填满了才往第二列跑,跟瀑布流那种优先横向填满再往下排的思路完全是两码事。

后来有人脑洞大开,拿flexbox硬刚,让容器flex-direction: column,再用height去限制父容器高度,强行把内容挤成多列。但这玩法有个致命伤,一旦子元素高度不一,浏览器根本不知道下一个元素该放哪,最终效果往往惨不忍睹。

再后来CSS Grid出来,大家都觉得有救了。Tab Atkins-Bittner那时候分享过一个“黑客式”写法,先把容器设为grid: auto-flow dense 1px / <列宽>,然后给每个子元素强行指定grid-row: span <像素高度>。这种方法等于把每个元素的高度提前告诉网格系统,让浏览器根据这个高度去“拼图”。对于图片列表这种高度固定的场景勉强能用,但要是内容动态加载、高度不确定,这套玩法就直接歇菜。

绕不开的那几道坎

讨论来讨论去,大家发现核心问题就那么几个。首先,瀑布流布局本质上是单轴控制,元素在水平方向排完后,再根据前面元素的高度决定下一个该贴到哪个坑里,这跟Grid双轴控制的底层逻辑有点八字不合。当时Rachel Andrew就提到,这玩意儿的行为模式其实更像flexbox,因为它完全依赖子元素尺寸来动态定位。

然后就是语法命名,这事儿吵得最凶。有人主张在grid基础上加新属性,比如grid-template-rows: flow;有人觉得干脆另起炉灶,搞一套全新的display值;还有人像Dan Tonon那样建议给flexbox打补丁,引入flex-block-countflex-block-flow来控制列数和流向。每个方案听起来都有道理,但真要落地,要么跟现有规范打架,要么对开发者不够直观。

最要命的是,任何方案都必须处理好元素高度动态变化、跨列元素如何摆放、间隙怎么填满这些细节。David DeSandro当年做Masonry.js库的时候,给W3C提了一大堆实战建议,比如图片加载过程中布局怎么调整、多列跨度的元素该怎么处理,这些都是纯CSS方案绕不开的硬骨头。

吵了八年终于定调

就这么你来我往,从2017年一直吵到2025年初,中间Jen Simmons都忍不住在会议上吐槽,说这问题讨论五年了还没个结果。转机出现在2025年1月31日那场会议,CSS工作组最终拍板:复用grid的模板和定位属性来实现瀑布流布局。

这意味着什么?就是以后写瀑布流,所有grid那套模板定义、子元素定位的玩法都能直接搬过来用,只是排版算法从原来的双轴网格模式切换成单轴瀑布流模式。这对开发者来说简直太友好了,不用重新学一套新东西,原来怎么定义列宽、怎么设置间距,现在照旧。

搞定瀑布流布局的两个方案

方案一:等官方落地用grid-lanes

等到各大浏览器把grid-lanes属性实现后,代码写起来会非常简单。这里假设要做一个三列的图片瀑布流,图片宽度自适应,间距统一为1rem。

先看HTML结构,就是最普通的列表包裹着图片块。

<div class="waterfall">
  <div class="item"><img src="pic1.jpg" alt=""></div>
  <div class="item"><img src="pic2.jpg" alt=""></div>
  <div class="item"><img src="pic3.jpg" alt=""></div>
  <!-- 更多图片块 -->
</div>

关键在CSS部分,把容器的display属性设置为grid-lanes,瀑布流模式就激活了。然后像写普通网格一样,用grid-template-columns定义列宽,这里用repeat(auto-fill, minmax(240px, 1fr))实现响应式,每列最小240px,最大自动拉伸填满剩余空间。列之间的间距用gap控制。

.waterfall {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
}

这就完了?对,就这么简单。每个.item里的图片高度各异,浏览器会自动把新元素塞进当前列高度最短的那一列下面,完美实现错落效果。

有的场景可能希望优先填满行,而不是优先考虑列高平衡。这时候可以借助后续可能推出的item-flow属性。比如想让内容按列方向排列,填满一列再换下一列,并且用dense模式填补空隙,可以这样写:

.waterfall {
  display: grid-lanes;
  grid-template-columns: repeat(3, 1fr);
  item-flow: column dense;
  gap: 0.8rem;
}

这里item-flow: column dense表示子元素按列方向流动,并且允许自动调整顺序来填充留下的空隙。collapse关键字还能进一步控制如何处理跨列元素造成的空洞,让布局更紧凑。

方案二:等不及就用轻量级JS过渡

浏览器全面支持grid-lanes之前,还有一套几乎不依赖JavaScript的过渡方案,用起来也很顺手。这个方案的思路是先用grid把容器分成若干列,然后靠JS动态计算每列的高度,把新元素塞到当前最短的那一列里。

首先定义容器的列结构,用grid划分好列宽,但不设置行高,让每个子元素自然撑开。

<div class="masonry-js" id="masonryContainer">
  <div class="item"><img src="pic1.jpg" alt=""></div>
  <div class="item"><img src="pic2.jpg" alt=""></div>
  <div class="item"><img src="pic3.jpg" alt=""></div>
  <!-- 其他元素 -->
</div>

CSS部分先把容器设成网格,并声明列数。这里固定三列,每列宽度相等。

.masonry-js {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

重点在JS脚本。页面加载后,获取所有需要排版的子元素,再获取每一列的DOM元素。遍历每个子元素,找到当前高度总和最小的那一列,把元素追加进去,同时更新这一列的高度。

function initMasonry() {
  const container = document.getElementById('masonryContainer');
  const items = Array.from(container.children);
  const columnCount = 3;
  const columns = [];

  for (let i = 0; i < columnCount; i++) {
    const col = document.createElement('div');
    col.className = 'masonry-col';
    container.appendChild(col);
    columns.push(col);
  }

  items.forEach(item => {
    let minHeightCol = columns[0];
    let minHeight = minHeightCol.offsetHeight;

    columns.forEach(col => {
      if (col.offsetHeight < minHeight) {
        minHeight = col.offsetHeight;
        minHeightCol = col;
      }
    });

    minHeightCol.appendChild(item);
  });
}

window.addEventListener('load', initMasonry);

为了布局能适应图片加载完成后的高度变化,还得监听图片加载事件。给每个图片绑定load事件,当图片加载完成时,重新触发布局计算。

function refreshMasonry() {
  const container = document.getElementById('masonryContainer');
  const cols = document.querySelectorAll('.masonry-col');
  cols.forEach(col => {
    while (col.firstChild) {
      container.appendChild(col.firstChild);
    }
  });
  cols.forEach(col => col.remove());
  initMasonry();
}

function observeImages() {
  const images = document.querySelectorAll('.item img');
  images.forEach(img => {
    if (img.complete) {
      refreshMasonry();
    } else {
      img.addEventListener('load', refreshMasonry);
    }
  });
}

window.addEventListener('load', () => {
  initMasonry();
  observeImages();
});

这套方案的好处是只用了一点点JS,大部分样式还是CSS控制,性能损耗极小。等以后grid-lanes普及,直接把display属性一改,JS代码全删掉就行,迁移成本几乎为零。

未来已来

目前各大浏览器厂商已经动起来了。Chrome和Edge早先在Chromium 140里实现了display: masonry,现在正往grid-lanes切换。WebKit那边在Safari 17里用了display: grid来支持瀑布流,后续也会跟进。Mozilla在2020年就率先在Firefox里做了实验性支持,同样在调整中。

现在想尝鲜,可以去浏览器内部实验性功能里打开对应flag,或者直接在Canary、Nightly这类开发版里试。等个一年半载,估计就能在生产环境放心用grid-lanes了。到时候写瀑布流就跟写普通网格一样顺手,再也不用跟那些奇奇怪怪的hacks较劲。