玩转CSS瀑布流布局,grid-lanes新特性到底怎么用?

3,336字
14–21 分钟
in

说起网页上那些错落有致的图片墙,像是Pinterest那种一列一列长短不一的展示方式,这就是传说中的瀑布流布局。以前要实现它,要么靠JavaScript库一顿计算,要么用多列布局硬凑,总感觉差点意思。现在CSS终于要放大招了,直接给了一个叫grid-lanes的显示方式,专门来解决这种布局痛点。

目录

啥是瀑布流布局

瀑布流布局,简单说就是容器里的每个小方块按列排,高度可以不一样,但宽度保持一致,最后看起来就像瀑布一样往下淌。这种布局特别适合展示图片或者卡片内容,能充分利用垂直空间,让页面看起来没那么死板。

等不了新特性咋整

虽然display: grid-lanes;这行代码看着简单,但浏览器全面支持还得等一阵子。在它正式上岗之前,可以用几种成熟方案先顶上。

方案一:多列布局模拟瀑布流

column-count这个属性其实早就能实现类似效果,只不过它是先从上往下填满第一列,再跑到第二列,和瀑布流的从左往右填的顺序不太一样。

.waterfall {
  column-count: 3;
  column-gap: 1rem;
}

.waterfall > * {
  break-inside: avoid;
  margin-bottom: 1rem;
}

在HTML结构里,直接把所有卡片扔进这个容器就行。每个卡片加个break-inside: avoid;能防止图片被从中间截断。这种方法的优点是纯CSS,不用JavaScript,性能杠杠的。缺点也很明显,内容顺序是按列读的,如果第一列第一张图片下面有第二张,那它在视觉顺序上其实排在第一列第二行,但实际代码顺序却在第二列第一张的前面,这可能导致读屏软件或者键盘导航时顺序错乱。

动手操作时,可以先确定列数,根据屏幕宽度用媒体查询调整。如果卡片里有图片,记得给图片设置display: block;,不然底部会有多余空隙。还有就是如果卡片内容高度差异巨大,可能会出现某一列特别长的情况,这时候可以通过column-fill: balance;让浏览器尽量平衡各列高度,但这玩意儿兼容性一般,最好还是让卡片高度差别太离谱。

方案二:JavaScript辅助的近似方案

如果追求更完美的瀑布流顺序,还是得靠JavaScript。这里有一个轻量级的思路,用绝对定位重新排卡片位置,核心逻辑就是找到当前最短的那一列,把下一张卡片塞到它下面。

function initMasonry(containerSelector, columnCount = 3) {
  const container = document.querySelector(containerSelector);
  const items = Array.from(container.children);

  container.style.position = 'relative';

  let columns = Array(columnCount).fill(0);

  items.forEach(item => {
    const minHeight = Math.min(...columns);
    const minIndex = columns.indexOf(minHeight);

    item.style.position = 'absolute';
    item.style.width = `calc(${100 / columnCount}% - 1rem)`;
    item.style.left = `calc(${minIndex * (100 / columnCount)}% + 0.5rem)`;
    item.style.top = `${minHeight}px`;

    const itemHeight = item.offsetHeight;
    columns[minIndex] += itemHeight + 16;

    container.style.height = `${Math.max(...columns)}px`;
  });
}

调用的时候很简单,等页面加载完或者图片加载完成后执行。如果是动态加载内容,每次新加卡片时重新调用这个函数就行。

这里有几个细节要特别注意。item.offsetHeight需要在元素可见时才能正确获取,如果图片还没加载完就拿高度,算出来的位置可能不对。所以在图片完全加载后再执行这个函数比较稳妥,可以用window.onload或者IntersectionObserver来触发。还有就是在窗口改变大小时,需要重新计算所有位置,这时候要先把columns数组重置,清空所有卡片的内联样式,然后重新跑一遍逻辑。性能方面,如果卡片数量特别多,比如几百张,频繁操作DOM可能会卡,可以考虑用requestAnimationFrame来节流。

方案三:Flexbox加Grid的混合玩法

还有一种取巧的办法,把列拆成几个独立的Flex列,手动控制每列的卡片。这种适合后端能控制数据分列的情况,或者用JavaScript把数据分组塞到不同的列容器里。

<div class="fake-masonry">
  <div class="col"></div>
  <div class="col"></div>
  <div class="col"></div>
</div>
.fake-masonry {
  display: flex;
  gap: 1rem;
}

.col {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

JavaScript这边,需要先知道卡片数组和列数,然后循环把卡片分配到最短的那一列。分配逻辑不是简单的一人一个轮流,而是实时计算当前哪一列累计高度最小,就把下一张卡片塞进去。这个方法的好处是顺序完全可控,而且是真正的从左到右、从上到下填充,和瀑布流的阅读顺序保持一致。坏处是容器高度没法自动撑开,需要JS实时监听内容变化来调整父容器高度,不然会有内容溢出。

新特性grid-lanes到底咋写

回到正题,未来浏览器全面支持后,写瀑布流就一句话的事儿。

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

这个grid-lanes值的意思是让网格系统只关注列方向的行进,每列独立计算自己的内容高度,然后下一行直接从当前列的最低点开始接着放。和普通网格不一样,普通网格每行高度都一样,所有列必须对齐,瀑布流则完全打破了这种对齐限制。

如果还想控制更细致的填充行为,比如要不要优先填满缺口,可以用item-flow属性来配合。

.masonry-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  item-flow: dense;
  gap: 1rem;
}

这里的dense关键词能让浏览器尽量不留白,有合适的小卡片就往缝隙里塞。item-flow目前还在讨论中,但基本逻辑已经定了,可以设置排列方向、换行方式、填充策略和间隙大小。

浏览器实现进度上,Chrome和Edge已经从最初的display: masonry转向支持grid-lanes,Safari和Firefox也跟进了。现在想尝鲜的话,可以在浏览器里开启对应的实验性标志,或者用上面提到的JS方案过渡。毕竟等所有旧浏览器都淘汰,新特性完全普及,还得需要点时间。

那些年关于命名的拉锯战

有意思的是,就为了定下grid-lanes这个名字,CSS工作组吵了好几年。候选名单长得吓人:collapsed-gridgrid-stackpacked-gridstaggered-grid,甚至还有masonry-grid这种直白的。最后选了grid-lanes,意思是在网格系统里开辟了车道(lane),每条车道独立前进。这个命名逻辑挺形象,把每列想象成一条车道,车流(内容)往下走,互不影响。以后写代码看到这个名字,就知道它是干啥的了,不用再翻文档猜意思。

所以现在如果手头有项目急着用瀑布流,建议先上方案二或者方案一,把逻辑封装好,等grid-lanes正式可用时,直接替换CSS那一块,JS慢慢移除就行。毕竟用户体验不能等,能跑起来的才是好方案。