资讯详情

资讯详情

三行CSS实现瀑布流布局,告别JavaScript

做前端的这些年瀑布流布局需求就没断过。电商首页、图片素材站、文章卡片墙到处都要那种高低错落、像水流一样铺开的视觉效果。放在以前这活儿基本被 JavaScript 垄断稍微复杂一点还得引个 Masonry 库计算列高、维护数组、监听图片加载折腾半天就为了把几张卡片摆整齐。但这两年 CSS 阵营悄悄把这块短板补上了核心代码就三条样式不需要任何脚本纯静态页面也能直接上瀑布流。这篇文章就聊聊我实际使用中的思路、代码写法、适配细节和踩过的坑给还在用 JS 做瀑布流的朋友一个减负方案。1. 为什么 CSS 瀑布流“突然”能做了1.1 传统 CSS 布局最大的心结行高对齐先说一个大家熟知的痛点。Flexbox 和 Grid 虽然强大但本质上还是“行”的思想。一行里的元素默认在交叉轴上对齐要么拉伸成一样高要么顶部对齐然后整行高度由最高的那个元素决定。瀑布流恰恰反着来——每张卡片高度随意下面一行要继续往上补位不能因为旁边卡片矮就留下一大片空白。早期有人用 float 硬凑效果很勉强。每列宽度固定float 能勉强分成几列可高度不齐的时候底部参差不齐而且卡片顺序是横向走完一列才去下一列跟真瀑布流的纵向阅读习惯差了十万八千里。用 Grid 也没好到哪去标准 grid 模型每一行轨道高度一致即使grid-auto-rows设成min-content行内元素高度不一致时finder 还是会按最高元素撑起整行底部依然有空隙。说白了传统 CSS 布局模型里缺少一种让元素“被填进最短那一列”的天然机制。1.2 多列布局才是隐藏的主角CSS 里其实一直藏着一套报刊排版专用的column多列布局。报纸分栏的时候文章段落从上到下填满第一列然后自动流到第二列天然就是纵向填充的逻辑。瀑布流其实就是若干个“单列容器”并排放在一起每列内部各自从上往下排列卡片然后把不同高度的卡片均匀塞进各列——这不正是column天生擅长的活吗之所以以前没人把它当瀑布流用一是早期 column 对破碎内容的控制太弱卡片容易在列之间被硬生生切断二是大家习惯了“JS Masonry 才是瀑布流正统”没往这个方向想。直到这两年break-inside属性逐渐被浏览器稳定支持卡片不截断了列间距也能精细调节了用三行 CSS 搭瀑布流才真正变得可用。我最早是在内部后台的卡片面板试水的当天就把一个跑了两年的 JS 瀑布流页面重写了效果直接对齐。2. 三行核心代码实现纯 CSS 瀑布流2.1 完整代码先摆出来先放一个可以直接跑起来的最小示例。假设页面上有一堆.card宽度不定、高度不定图片撑开高度内容多少也不统一放进一个.masonry容器里div classmasonry div classcard img src1.jpg alt p这是第一张卡片的内容高度自然撑开。/p /div div classcard img src2.jpg alt p第二张卡片内容更长在列内往下延伸。/p /div !-- 更多卡片... -- /div.masonry { columns: 4; column-gap: 16px; } .card { break-inside: avoid; margin-bottom: 16px; }严格来说你真正需要用到的核心 CSS 规则就三条columns: 4、column-gap: 16px、break-inside: avoid第一行columns甚至能继续简写成column-count: 4。加上卡片底部间距整个瀑布流就成了零 JavaScript。浏览器渲染时自动计算每张卡片该进哪一列哪列矮往哪填顺序从上到下。实测在常规内容页里几十张卡片的布局计算耗时可以忽略不计。2.2 逐行拆解每条规则的意图columns: 4看起来只是把容器平均分成四列实际上它内部隐含了一个不太直观的逻辑内容会优先填满第一列再流向第二列以此类推。也就是说卡片在 DOM 里的排列顺序就是“第一列的从上到下然后第二列的从上到下”而不是第一行排完四张再排第二行。懂了这个方向后面处理动态加载和数据顺序时就心里有数。break-inside: avoid是最容易漏但最要命的一条。它的作用通俗讲就是“别把一张卡片拆成两半塞到两列里去”。没有这条规则当卡片底部恰好落在列边界时浏览器会把卡片内容拦腰截断上半截在上一列下半截跑下一列页面直接没法看。这里多说一句旧浏览器可能需要加-webkit-column-break-inside: avoid前缀才能生效我在一些老的安卓 WebView 上确实遇到过但现在主流浏览器直接用标准属性就够了。column-gap负责列间距那卡片上下间距怎么控制呢我用的是margin-bottom这是最稳的做法。你也可以给卡片套个内边距容器或者在卡片内部再包一层统一间距但会多一层 DOM。直接动用margin-bottom简单粗暴而且因为每张卡片本身是独立的块级盒子在瀑布流纵向排列时margin正常生效不会出现外边距折叠问题。2.3 卡片内图片需要顺手处理的细节卡片里只要放了图片有几个小坑几乎必踩。第一是图片底部会出现一条四五像素的缝隙这是图片作为内联元素留下的基线空隙最常见的解法是让图片display: block顺手加一句.card img { display: block; width: 100%; }第二是图片还没加载完成时卡片高度是 0瀑布流看起来全是塌陷的方块。传统 JS 方案需要监听load事件然后重新排版CSS 方案虽然也能在布局阶段拿到图片最终的占位尺寸吗实际上columns布局遇到图片加载前后的高度变化是会自动回流重排的因为浏览器在图片加载完成后会触发布局更新但如果你希望规避加载闪动可以在图片外层加一个固定宽高比的占位容器。现在也可以用aspect-ratio给图片设定宽高比给容器卡位.card img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }aspect-ratio在图片还没加载时就会撑出比例空间加载完成后高度不跳变。这个技巧在文章列表里的封面图上尤其好用配合object-fit: cover就算源图比例不统一也不会把布局撑得乱七八糟。3. 适配不同场景的进阶技巧3.1 响应式列数两种思路各有利弊固定写死columns: 4在桌面端没问题换成手机屏幕就太挤了。最简单粗暴的方式是加媒体查询断点内写死不同的列数.masonry { columns: 1; } media (min-width: 600px) { .masonry { columns: 2; } } media (min-width: 900px) { .masonry { columns: 3; } } media (min-width: 1200px) { .masonry { columns: 4; } }这套写法直白适合卡片宽度相对固定的场景。另一种思路是使用column-width.masonry { columns: 300px; }这句的意思是“每列尽量 300px 宽能塞几列就塞几列”具体列数由浏览器根据容器宽度自动决定。比如 1000px 宽的容器浏览器就排三列多出的宽度分摊给列间距。这个方案响应式是天然实现的不需要写任何媒体查询移动端和桌面端都能自适应缺点是你对列数失去精确控制窄屏时可能出现两列、三列来回跳的情况。我个人的习惯是页面结构固定的项目用column-count加断点内容流特别复杂的用column-width图省事。3.2 想要传统瀑布流的横向顺序怎么办前面提过CSS column 瀑布流默认是“纵向填充”顺序。如果图片流场景里你需要第一张图排在左上角、第二张排在第二列顶部这种“横向先走”的效果纯 column 是做不到的。很多人第一次用的时候都会觉得顺序很别扭因为 DOM 里的第 2 张卡片不一定会出现在右边的列而是掉到第一列第二行。如果你的产品需求不是严格的“时间倒序、最新图必须在最左上角”其实纵向顺序影响不大。真遇到对顺序敏感的场景我的经验是可以在数据层做一次“按列重排”——把数组按列数拆成几组比如 4 列就把数据分成 4 组第一组在前第二组在后间接实现横向顺序。但这会破坏“三行代码搞定”的初衷所以建议一开始就评估清楚需求排序的重要程度。正经图片瀑布流 App 那种“加载更多后新图插到最顶上”的交互用 CSS 方案基本无解还是回去用 JS 靠谱。3.3 动态加载更多内容时布局会自动更新用 JS 方案做“滚动到底部加载更多”时需要手动调用masonry.layout()方法告诉库重新计算位置。CSS 方案不需要这一步——你把新节点追加到容器里浏览器会自动把新卡片塞进当前最矮的列。这个特性在开发体验上属于降维打击你只管拼字符串、插入节点剩下的全是浏览器自己的渲染工作。但有一点要注意如果新卡片里还有图片且图片没有占位尺寸加载过程会触发布局抖动。列数少还看不出来列数多的时候能明显看到卡片跳来跳去。我一般会给动态加载的卡片统一用 JS 生成一个aspect-ratio占位样式或者直接约定封面图比例统一从源头规避抖动。3.4 空列问题与底部对齐处理column布局在卡片数量不足时可能出现最后一列空着或者最后几张卡片分布得参差不齐、底部有很大的空白块。这个问题在 JS 方案里也不少见因为每列高度天然不可能完全对齐。实用一点的兜底做法是不要追求完美的底部平齐瀑布流的美感本来就在错落如果卡片量实在太少可以适当减少列数比如 2 列就比 4 列更容易让视觉显得饱满。还有一个小技巧给容器设定一个最小高度min-height让少量卡片时布局也能撑满视觉区域不至于显得特别空。4. 和 JavaScript 方案的对比与选型建议4.1 一张表格看清单方差异维度CSS column 方案JavaScript 传统方案代码量三行核心 CSS监听、计算、DOM 操作动辄几十行布局性能浏览器原生渲染无重排脚本每次内容变化都要跑一次布局计算排序方向纵向填充列优先可由配置指定横向/纵向动态加载追加节点自动布局手动调用 layout 接口过滤/排序无法平滑动画过渡可以 FLIP 动画、插入移除过渡虚拟滚动不支持已有成熟库方案兼容性现代浏览器均可全覆盖看这张表就知道这个对比不是“谁替代谁”而是应用场景完全不同。纯粹的卡片展示页、图片墙、文章列表用 CSS 方案就够了少写代码少维护一个库页面还轻。但你要是做个后台编辑器卡片需要拖拽排序、过滤筛选时带动画或者一次加载几千条数据需要虚拟滚动那 JS 方案依然不可替代。4.2 加载性能与渲染性能谁更好有朋友问过我CSS 方案不也要浏览器自己去计算分列吗和 JS 算有什么差别差别大了。JS 方案里你要维护一个数组记录每列高度每插入一张卡片都要遍历比较各列高度然后通过transform或绝对定位把卡片塞进去过程中可能触发多次强制回流。CSS 方案是浏览器渲染引擎里的布局模块直接处理算法层面比脚本模拟高效得多而且不需要手动监听图片加载。实测在 200 张卡片、开启性能面板的情况下CSS 方案基本能做到布局不耗时JS 方案在低端机上偶尔会看到白屏闪烁。不过也别把 CSS 吹上天。如果你做的是电商那种“筛选品牌、排序价格”的交互页面每次筛选结果变化时卡片列表整个重排CSS 方案是做不到平滑动画的——因为列布局下元素位置由浏览器决定你很难从 DOM 中精确知道每张卡片从哪列移到了哪列。此时还是乖乖用 JS 库做 FLIP 动画吧视觉体验截然不同。4.3 兼容性现状与降级策略columns属性本身是 CSS 多列布局里的老成员IE10 都认识它浏览器支持层面没有大问题。真正决定是否能展示正常瀑布流的还是break-inside: avoid它在 Chrome 85、Firefox 60、Safari 14 上都是标准行为覆盖现役用户绰绰有余。如果你不得不兼容极其老旧的浏览器降级策略也很简单不用break-inside的话卡片被拆分但布局还是有的大不了视觉丑一点或者用supports判断一下不支持的场景切回普通网格布局.masonry { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } supports (columns: 4) and (break-inside: avoid) { .masonry { display: block; columns: 4; } .masonry .card { break-inside: avoid; } }这样老浏览器走规整的四列网格新浏览器走瀑布流内容完整不算好看但能用。4.4 新特性展望Grid 原生 Masonry除了 column 方案CSS Grid 也在草案阶段讨论过原生的 masonry 布局grid-template-rows: masonry能直接让 grid 子项目按瀑布流方式填充。Firefox 和 Chrome 都拿它做过原型但至今还没有浏览器正式默认支持日常项目里不建议等它。相比之下 column 方案现在是“即拿即用”的稳定选择我自己的看法是除非未来某天 gridding masonry 全面落地否则目前动手项目优先选 column不要被新特性吊着胃口。5. 实操中常见的坑与排查技巧5.1 卡片内容被拦腰截断这是新手用 CSS 瀑布流遇到最多的 bug表现形式是内容从一列串到另一列、文字从中间断开。原因只有一个忘了写break-inside: avoid。有些时候写了但没生效可以检查一下是不是被其他样式覆盖了比如某些 CSS 框架里给块级元素加了break-inside: auto之类的规则。优先排查仍不行的话给卡片套一个内层div把break-inside: avoid加在内层上.card-inner { break-inside: avoid; }这个方法做了层兜底因为内外两层同时被截断的概率极低。5.2 列数或列宽跟想象中不一样使用了columns: 4却只显示三列检查容器宽度是不是不够。如果卡片内部有固定最小宽度比如一张 200px 的图片浏览器为了保证卡片可读性可能自动减少列数。想强制列数的话还要配合设置容器的总宽度或者去掉卡片内部的固定宽度限制。反过来卡片太宽导致一列都塞不下也会出现布局崩坏此时检查max-width优先于看 column 的锅。5.3 卡片间出现莫名的间距不一致CSS 瀑布流里卡片之间的垂直间距是margin-bottom水平间距是column-gap这两个值如果不一样视觉上会横竖不均看久了很难受。我建议把这两个值设成同一个 CSS 变量比如:root { --masonry-gap: 16px; } .masonry { column-gap: var(--masonry-gap); } .card { margin-bottom: var(--masonry-gap); }这样改一处横竖间距同步变化不会再出现“上间距 16 右边距 12”的尴尬情况。还有一点容易疏忽column-gap也会影响卡片内部的多列文本但瀑布流场景里卡片内部没有特殊列布局的时候不用操心。5.4 容器高度异常或滚动条反复跳动页面一开始就出现一个很高的空白块然后图片一张张加载完才慢慢缩回去这多半是图片没有占位尺寸浏览器在图片加载前按 0 高度计算了整个瀑布流高度。前面提过用aspect-ratio能解决大部分场景但有些动态图片地址拿不到比例就无法预知占位高度。此时通用的补救方案是给图片标签加一行loadinglazy让浏览器按懒加载策略处理配合 CSS 里给图片一个最小高度.card img { min-height: 120px; background: #f0f0f0; }加载前至少有个灰块占位加载完成后高度变化很小视觉上不太突兀。5.5 瀑布流容器最后一行左右明显不对称有的页面看完所有卡片后最后一列特别长其他列都空了一大截。这通常是卡片总数和列数的整除关系导致的不是 bug是分布的自然结果。最简单的兜底方案是动态控制列数卡片少的时候用 2 列多了再撑到 4 列。纯 CSS 做不到按数量切换需要写几行 JS 判断容器里卡片数量后动态设置类名。既然标题是“告别 JavaScript 布局”这点小判断并不会拖累整体加一个类名切换的成本几乎为零const container document.querySelector(.masonry); const count container.children.length; container.classList.toggle(few-items, count 8);.masonry.few-items { columns: 2; }这套组合方案在实际项目里既保住了 CSS 布局的简洁又解决了极端数量下的观感问题。5.6 动态加载后偶发卡片重叠理论上浏览器会自动重排但某些情况下比如图片缓存、异步渲染顺序不同动态插入节点时会短暂出现卡片重叠。这种现象大多是因为在同一个事件循环里同时插入了多个带图片的卡片浏览器渲染时有中间态。我试下来最靠谱的解法是让每个新卡片先visibility: hidden用动画帧推后两帧再显示.card-enter { visibility: hidden; }requestAnimationFrame(() { requestAnimationFrame(() { card.classList.remove(card-enter); }); });这样给浏览器留出重排的余量卡片出现时已经是稳定位置不会闪烁也不会重叠。这个技巧谈不上漂亮但胜在稳定是我在多列布局项目里长期使用的土办法。最后分享一点个人体会把瀑布流从 JS 换成 CSS 的这一年里我最大的感受是布局这事能交给浏览器原生能力就别自己造轮子。CSS column 虽然是个老东西但在合适的场景下它比 Masonry 库更轻、更快、更省心。不过它也谈不上万能遇到排序、过滤、动画这类强交互需求时照样得回头找 JavaScript。我给自己的选型标准很简单纯展示型内容墙无脑上 CSS要频繁动元素位置就老实接 JS 库别两头硬凑。如果你正在维护一个沉重的 JS 瀑布流页面不妨挑个不重要的模块试试这三行代码实测一下性能提升心里自然就有答案了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →