Zane-Cody's Hoard
回到归档

照片墙的瀑布流是 CSS 做的,一行 JS 都没有

3 分钟#CSS#设计

用 CSS 多栏(columns)实现混合宽高比的照片墙,比 JavaScript 算位置简单得多,而且不会有一帧的跳动。

照片墙里既有横图(1400×900)、竖图(900×1300)也有方图(1100×1100)。要把它们排得好看,通常有两条路:

  1. 用 JavaScript 算出每张图的位置
  2. 让 CSS 自己算

第二条路只要三行。

columns 方案

.gallery {
  columns: 2;        /* 窄屏两栏 */
  column-gap: 0.75rem;
}
@media (min-width: 640px) {
  .gallery { columns: 3; }
}

.tile {
  break-inside: avoid;   /* 关键:别让一张图被拆到两栏 */
  margin-bottom: 0.75rem;
}

原理是 CSS 多栏布局会把内容按高度尽量均匀地分配到各栏里 —— 这正好就是瀑布流想要的效果,不用自己算。

配上 Tailwind 就是:

<div class="gap-3 sm:columns-2 lg:columns-3">
  <button class="mb-3 block w-full break-inside-avoid">…</button>
</div>

为什么不用 JS 方案

除了少写代码,还有一个不太被提起但很重要的好处:没有布局跳动。

JS 方案必须先渲染一遍、测量、再定位。用户看到的顺序是:

先看到图片挤成一列  →  突然跳成瀑布流

慢一点或者图多一点的时候,这一下很明显。CSS 方案在首帧就是最终布局,因为它压根没经过"中间状态"。

宽高比要提前写死

有个前提:每张图必须知道自己的宽高比,否则占位高度是 0,图片加载完会撑开容器、把下面的内容推下去 —— 还是跳。

所以内容数据里老老实实记了尺寸:

{
  "type": "image",
  "src": "/media/photos/p11.webp",
  "width": 1400,
  "height": 900
}

渲染时写成:

<button style="aspect-ratio: 1400 / 900">

容器的高度在第一帧就定死了,图片什么时候加载完都不影响布局。

想让页面不跳,靠的不是"加载得快",而是"提前知道自己长多高"。

视频也走同一套

视频元素同样能设置 aspect-ratio,所以图片和视频混在同一个网格里完全不需要特殊处理。视频额外做了两件事:

  • 缩略图上 muted loop playsInline,鼠标悬停才 play(),移开就暂停并回到开头
  • 列表里只加载元数据(preload="metadata"),不预加载整个视频文件

十个视频全预加载的话,页面能有好几十 MB。这种事只能靠克制。