纯 CSS 实现横向排序的瀑布流式布局

Index

难点在哪

那么如何用纯CSS实现？

用 flexbox, :nth-child() 和 order 实现 CSS 瀑布流式布局

现状：鱼和熊掌不可兼得，要么排列乱序，要么间距诡异

使用 order 和 nth-child() 重新排序

防止列合并

结论

超过三列的瀑布流

这个方法不适用于……

前阵子在写一个图片选择器时，想实现纯 CSS 对图片进行瀑布流式排版 (Masonry Layout)。一个合格的纵向瀑布流式布局包含以下几个条件：

每个内容块高度可以不等，但宽度相等。
由于内容的不确定性，内容块的高度应根据内容高度伸缩。高度相等的话就变成了网格布局，规整倒是规整，不仅没有瀑布效果，内容的个性也无从体现。

内容块应进行横向排序。
由于是纵向瀑布流式布局，用户的浏览顺序自上而下。加载的新内容始终排列在最下方，因此整个布局的高度可以无限延展，而宽度始终固定。这就要求内容在有排序需求时，必须从左到右依次填充页面。

内容块列数固定。
内容块的列数应是可控的，在当前 viewport 下不会因为容器空间不足造成内容块溢出或缩小。三列的瀑布流，就应该始终是三列。

难点在哪

对瀑布流式布局进行稍加研究的话就会发现，使用 display: grid 无法实现 1 的效果，而使用 display: flex + 多列布局 (multi-columns) 也无法达到 2 的要求（下文将有具体描述）。由于缺乏原生支持，长期以来各类号称“纯 CSS 制作瀑布流布局”的解决方案并没有哪个能真正满足以上所有条件，最后大家只能作罢，投靠 JS 库。

那么如何用纯CSS实现？

更新于2020-10-26:

好消息是：Wes Bos 在推特上预告 CSS Grid Level 3 将支持瀑布流布局。 坏消息是：它还处于草稿阶段，目前没有浏览器支持。

.grid {

display: inline-grid;

grid: masonry / repeat(3, 2ch);

border: 1px solid;

masonry-auto-flow: next;

}

期待能用上它的一天。

上个月发现一篇迂回实现的文章，用 :nth-child() 和 order 解决了 flexbox 无法正确排序的问题。私以为这个技巧简单且巧妙，一不小心激动了一番，觉得不分享给中文圈里的更多人知道实在太可惜。决定和作者沟通后翻译出来，于是——

以下文章的英文原文作者为 Tobias Ahlin，已经作者允许翻译并转载于本站。由于极少接触中文 HTML/CSS 术语，解释和翻译中有误之处还请不吝勘正。

用 flexbox, :nth-child() 和 order 实现 CSS 瀑布流式布局

用 flexbox 制作瀑布流布局乍看似乎很容易：只要用 flex-flow: column wrap 就能实现。问题在于这个方法实现出的内容块会排序错乱：内容块渲染是由上至下，而用户阅读是由左至右，因此用户看到的内容块顺序可能是1, 3, 6, 2, 4, 7, 8, 5之类的。

在 flexbox 里用 column 布局实现在 row 才能达到的排序绝非易事，但加上 :nth-child() 和 order 这两个属性就能做到不依靠 JavaScript ，仅用CSS实现瀑布流式布局。

先上干货总结：假设要渲染三列布局，用 flex-direction: column 实现 row 排序的话，只需要：

/* 让内容按列纵向展示 */

.container {

display: flex;

flex-flow: column wrap;

}

/* 重新定义内容块排序优先级，让其横向排序 */

.item:nth-child(3n+1) { order: 1; }

.item:nth-child(3n+2) { order: 2; }

.item:nth-child(3n) { order: 3; }

/* 强制使内容块分列的隐藏列 */

.container::before,

.container::after {

content: "";

flex-basis: 100%;

width: 0;

order: 2;

}

就能实现出以下效果。其中灰色分隔线是刚才强制使内容块分列（英文）的两个伪元素。

其中灰色分隔线是刚才强制使内容块分列的两个由伪元素组成的列

现状：鱼和熊掌不可兼得，要么排列乱序，要么间距诡异

Flexbox 并不是为瀑布流布局而生。如果给 flex 容器设置一个固定高度（这样内容在溢出时会自动换列）并加上 flex-flow: column wrap, 会得到以下效果：

内容块自上而下渲染，因此从左往右阅读时会以为内容是乱序排列的。在很多场景下这种结果已经能满足需求，但对序列有要求时这样写只会随着内容的增多而愈显混乱。

如果改为 flex-direction: row 而内容块的高度又不一致时，虽然能够得到正确的顺序，内容块间的间距却无法把控。

果然是鱼和熊掌吧。如果用 flex-direction: column 并在 HTML 中移动内容块元素的位置，虽然可以达到最终效果上的正确排序，却极其麻烦，还会造成使用 tab 键导航时的混乱。

使用 order 和 nth-child() 重新排序

order 属性能影响 flexbox 或 grid 布局中的子项。使用起来很直观：如果两个元素之一属性为 order: 1 而另一个为 order: 2， 那么 order: 1 的元素会无视它在 HTML 里的源代码顺序，被重新渲染并排列在另一个元素前面。

这个解决方案仰仗 order 属性定义里的一个细节: 如果两个或多个内容块有同样等级的 order 时怎么处理？哪个排前面？这种情况下，在 flexbox 中排序会回溯元素在HTML源代码里的顺序：源代码里排序靠前的优先渲染。正是这个细节预留了对内容块重新排序的可能性，即使内容块初始时以纵向排序，也能配合使用 nth-child() 让它重新打横排列。

参见下表：当我们谈论内容块按 flex-direction: row 的效果排序时，指的是让它们按默认顺序：1, 2, 3, 4, 5, 6……排列。

| 列1 | 列2 | 列3
行1 | 1 | 2 | 3
行2 | 4 | 5 | 6
行3 | 7 | 8 | 9
行4 | 10 | 11 | 12

如果用 flex-direction: column 实现同样的排序，每列的内容块应该分配和以上相同的序号。换句话说，给第一列内容块分别分配序号 1, 4, 7, 10，第二列 2, 5, 8, 11，第三列 3, 6, 9, 12。这时选择器 nth-child() 就派上用场了，我们可以用它来选择应该排在第一列的内容块为 (3n+1), 第二列为 (3n+2), 第三列为 3n, 并给同一列的内容块加上同样的 order 值。以第一列为例：

/* 第1列 */

.item:nth-child(3n+1) { order: 1; }

这时选择器将选择 flexbox 容器内第 1, 4, 7, 10 个元素，即：选中整个第一列。换言之，用 nth-child() 和 order 根据元素原始顺序进行重排。第二列和第三列以此类推：

.item:nth-child(3n+1) { order: 1; }

.item:nth-child(3n+2) { order: 2; }

.item:nth-child(3n) { order: 3; }

这里我们给第一组：第 (3n+1) 个内容块赋上 order:1；第二组：第 (3n+2) 个内容块（下称第二组）赋上 order:1；第三组：第 (3n) 个内容块（下称第三组）赋上 order:3。这时整体顺序应变为：1, 4, 7, 10, 2, 5, 8, 11, 3, 6, 9, 12。

如果我们能确保每一组内容块独占一列（不换列），就能在从左到右阅读时营造出横向排序的效果。

这么做会影响使用 tab 键导航的顺序吗？完全不会。 order 只改变元素视觉呈现效果，不改变 tab 顺序。

防止列合并

如果瀑布流布局内放置了太多内容块，这个方法最终会崩坏。我们理想化地认为每一组会被渲染为一列，但实际上由于每个内容块高度不一致，其他列的内容块很可能合并到前一列去。举个例子：第一列可能比其他两列要长很多，导致第三列的头跑到第二列的末尾去：

第一列可能比其他两列要长很多，导致第三列的头到第二列的末尾去

高亮的内容块 (3) 理应堆叠在第三列头部，否则会导致整个布局错乱。但由于第二列尾部还有足够空间，它自然而然就续在第二列尾部了。

为了解决拆列 (wrapping) 问题，我们可以干预什么情况下换列。Flexbox 并不提供“内容从这里开始换到新的一列”的原生支持，但我们可以通过添加高度 100% 的不可见元素作为来达到这一效果。正因为元素占了容器 100% 高度，它无法被纳入某一特定列中，只能自成一列，因此能达到强制换列的效果。

这些不可见的分隔线需要成为内容块元素数组的一部分，使数组有这样的顺序：1, 4, 7, 10, <分隔线>, 2, 5, 8, 11, <分隔线>, 3, 6, 9, 12。要达到这种效果，可以在容器上：

添加两个伪元素 :before 和 :after
添加后这两个伪元素会分别成为容器的第一个和最后一个子元素，DOM 里的顺序如下：

|-- 容器
|-- :before
|-- 内容块
|-- 内容块
|-- ...
|-- :after

让伪元素的 order 等于 2 视觉渲染上它们在会成为第二组内容块的第一个和最后一个元素：:before, 2, 5, 8, 11, :after。

/* 换列的分隔线 */

.container::before,

.container::after {

content: "";

flex-basis: 100%;

width: 0;

order: 2;

}

为体现效果，下图两个伪元素高亮展示。注意，即使3号内容块的高度允许它被堆叠在第二列，此时它也会被渲染为第三列的第一个元素。

即使3号内容块的高度允许它被堆叠在第二列，它也会被渲染为第三列的第一个元素。

结论

最后一步，确保容器的高度要大于最长的列的列高（否则列会溢出）。至此，就实现了一个仅用 CSS 写出的三列的瀑布流了。

https://codepen.io/jessuni/pen/GaOPVz

超过三列的瀑布流

要使用同样方法实现三列以上的瀑布流，需要做以下变动：改变排序算法，调整内容块的高度，手动增加换列元素（而不是用伪元素）。3、4、5、6 列的瀑布流布局效果可以参见这个 codepen 集（英文）。

基于只能添加两个伪元素 :before 和 :after 的限制，这里我们只能先手动添加分隔线元素（分隔线的数量要比列数少一个）到容器内的末端，然后对它们进行排序：











...









我们必须找到一个方法让分隔线不参与内容块的排序，而是内容块和分隔线分别进行各自的内部排序。这里我们用 span 制作分隔线，以便稍后单独选出来做排序。由于 nth-of-type 可以选中同类型的标签，我们可以用它来对内容块和分隔线进行分别排序：

.item:nth-of-type(4n+1) { order: 1; }

.item:nth-of-type(4n+2) { order: 2; }

.item:nth-of-type(4n+3) { order: 3; }

.item:nth-of-type(4n) { order: 4; }

分隔线元素，和前面一样，占据容器 100% 的高度：

/* 强制换列 */

.break {

flex-basis: 100%;

width: 0;

margin: 0;

}

由此形成 4 列的瀑布流布局。

https://codepen.io/jessuni/pen/KLybGw

这种纯CSS实现瀑布流的方法虽然不如用 JavaScript 实现（比如 Masonry）那么灵活，但你如果不想实现一个瀑布流布局还要依赖第三方库的话，这个技巧能派得上用场。

如果你需要更多关于常见的 CSS flexbox 布局的帮助，可以参考可以复制粘贴进项目里的一些 flexbox 例子（英文）和深度解析 flexbox 中使用分隔线的技巧（英文）。

（完）

这个方法不适用于……

这个方法的美好建立在对瀑布流式布局没有太多要求的基础上。但如果你：

需要无限加载内容：这时就必须引入 JS 去计算每一列的动态高度，并保证容器的动态高度始终大于每一列的列高。

列数做响应式处理：根据 viewport 适配并展示不同列数时，每次都要做计算并展示/隐藏分隔线，要重复写好几套 media queries。

如果 1 + 2 都要满足，就真的特别蛋疼还不如自己写一个库算了。

不巧的是，我的需求正好是 3，一番折腾后觉得划不来，最后还是用了个轻量的 Macy 来解决。如果你在用 Typlog 的话，就是编辑文章里从本地/unsplash 添加图片的那个功能了。

如果你的需求恰好非常幸运地避开以上一点或多点，只需要一点点 JS 甚至根本不用，就能通过这个方法完美实现。