2022-04-19 纯 CSS 实现瀑布流式排版

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


  • 1、每个内容块高度可以不等,但宽度相等。
    由于内容的不确定性,内容块的高度应根据内容高度伸缩。高度相等的话就变成了网格布局,规整倒是规整,不仅没有瀑布效果,内容的个性也无从体现。
  • 2、内容块应进行横向排序。
    由于是纵向瀑布流式布局,用户的浏览顺序自上而下。加载的新内容始终排列在最下方,因此整个布局的高度可以无限延展,而宽度始终固定。这就要求内容在有排序需求时,必须从左到右依次填充页面。
  • 3、内容块列数固定。
    内容块的列数应是可控的,在当前 viewport 下不会因为容器空间不足造成内容块溢出或缩小。三列的瀑布流,就应该始终是三列。

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

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

.grid {
 display: inline-grid;
 grid: masonry / repeat(3, 2ch);
 border: 1px solid;
 masonry-auto-flow: next;
}

期待能用上它的一天。

用 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 属性能影响 flexboxgrid 布局中的子项。使用起来很直观:如果两个元素之一属性为 order: 1 而另一个为 order: 2, 那么 order: 1 的元素会无视它在 HTML 里的源代码顺序,被重新渲染并排列在另一个元素前面。

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

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

如果用 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:2;第三组:第 (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。要达到这种效果,可以在容器上:

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

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

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

/* 换列的分隔线 */
.container::before,
.container::after {
  content: "";
  flex-basis: 100%;
  width: 0;
  order: 2;
}

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

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

结论

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


<div class="container">
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 190px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 120px"></div>
  <div class="item" style="height: 160px"></div>
  <div class="item" style="height: 180px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 150px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 170px"></div>
</div>
.container {
  display: flex;
  flex-flow: column wrap;
  align-content: space-between;
  /* 容器必须有固定高度
   * 且高度大于最高的列高 */
  height: 660px;
  
  /* 非必须 */
  background-color: #f7f7f7;
  border-radius: 3px;
  padding: 20px;
  width: 60%;
  margin: 40px auto;
  counter-reset: items;
}

.item {
  width: 32%;
  /* 非必须 */
  position: relative;
  margin-bottom: 2%;
  border-radius: 3px;
  background-color: #a1cbfa;
  border: 1px solid #4290e2;
  box-shadow: 0 2px 2px rgba(0,90,250,0.05),
    0 4px 4px rgba(0,90,250,0.05),
    0 8px 8px rgba(0,90,250,0.05),
    0 16px 16px rgba(0,90,250,0.05);
  color: #fff;
  padding: 15px;
  box-sizing: border-box;
}

 /* 仅用于打印数字 */
.item::before {
  counter-increment: items;
  content: counter(items);
}

/* 将内容块重排为3列 */
.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;
}

body { font-family: sans-serif; }
h3 { text-align: center; }

在线效果演示:
https://codepen.io/jessuni/embed/GaOPVz/?height=360&theme-id=0&default-tab=result

超过三列的瀑布流

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

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

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

<div class="container">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>

  <span class="item break"></span>
  <span class="item break"></span>
  <span class="item break"></span>
</div>
.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 列的瀑布流布局。

<div class="container">
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 190px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 120px"></div>
  <div class="item" style="height: 160px"></div>
  <div class="item" style="height: 180px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 150px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 190px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 120px"></div>
  <div class="item" style="height: 160px"></div>
  <div class="item" style="height: 180px"></div>
  <div class="item" style="height: 140px"></div>
  <div class="item" style="height: 150px"></div>
  <div class="item" style="height: 170px"></div>
  <div class="item" style="height: 170px"></div>
  
  <span class="item break"></span>
  <span class="item break"></span>
  <span class="item break"></span>
</div>
.container {
  display: flex;
  flex-flow: column wrap;
  align-content: space-between;
  /* 容器必须有固定高度
   * 且高度大于最高的列高 */
  height: 960px;
  
  /* 非必须 */
  background-color: #f7f7f7;
  border-radius: 3px;
  padding: 20px;
  width: 60%;
  margin: 40px auto;
  counter-reset: items;
}

.item {
  width: 24%;
  /* 非必须 */
  position: relative;
  margin-bottom: 2%;
  border-radius: 3px;
  background-color: #a1cbfa;
  border: 1px solid #4290e2;
  box-shadow: 0 2px 2px rgba(0,90,250,0.05),
    0 4px 4px rgba(0,90,250,0.05),
    0 8px 8px rgba(0,90,250,0.05),
    0 16px 16px rgba(0,90,250,0.05);
  color: #fff;
  padding: 15px;
  box-sizing: border-box;
}

 /* 仅用于打印数字 */
div.item::before {
  counter-increment: items;
  content: counter(items);
}

/* 将内容块重排为4列 */
.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; }

/* 强制换列 */
.break {
  flex-basis: 100%;
  width: 0;
  border: 1px solid #ddd;
  margin: 0;
  content: "";
  padding: 0;
}

body { font-family: sans-serif; }
h3 { text-align: center; }

在线效果演示:
https://codepen.io/jessuni/embed/KLybGw/?height=360&theme-id=0&default-tab=result

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

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

补充:

这个方法不适用于……

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

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

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

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

不巧的是,我的需求正好是 3,一番折腾后觉得划不来,最后还是用了个轻量的 Macy 来解决。

原文地址:https://tobiasahlin.com/blog/masonry-with-css/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 纯 css 写瀑布流 1.multi-columns 方式: 通过 Multi-columns 相关的属性 col...
    王远清orz阅读 2,259评论 1 30
  • 弹性盒模型的一些知识 一、简单介绍   弹性盒模型( Flexible Box或FlexBox)是一个CSS3新增...
    Yafine阅读 267评论 0 0
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,480评论 2 59
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,630评论 1 92
  • 目录 Day01标签行元素 Day02表单元素css选择器伪类选择符行内元素块元素表格 Day03文本相关属性列表...
    Moquyun阅读 482评论 0 0