在纯CSS里实现瀑布流,核心矛盾在于:既要让卡片按宽度分列,又要保留每张卡片原始高度形成的错落排列。Flexbox本身擅长一维布局,通过横向主轴加纵向换行,可以模拟出多列效果,再借助媒体查询在不同屏幕下改变列宽与列数,就能控制整体布局密度。

一、Flexbox实现瀑布流的基本结构
最常见的误区是直接使用一个Flex容器包裹所有卡片,并期望它们像Pinterest那样上下错开。实际上,若容器为display:flex且flex-wrap:wrap,卡片会优先填满一行后再换到下一行,同一行的卡片高度会被拉伸对齐,这就不是瀑布流了。正确做法是将容器设为横向排列的多列,每一列自身是一个纵向Flex容器,卡片在列内正常堆叠。
下面给出一个简化的HTML结构,外层是列容器,内层是每个卡片列:
<div class="waterfall">
<div class="column">
<div class="card">卡片1</div>
<div class="card">卡片2</div>
</div>
<div class="column">
<div class="card">卡片3</div>
<div class="card">卡片4</div>
</div>
<div class="column">
<div class="card">卡片5</div>
</div>
</div>
对应的CSS让外层.waterfall成为横向Flex容器,每个.column占固定比例宽度并纵向排列卡片。这样每一列独立增高,不会出现行内等高问题。
.waterfall {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 16px;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #f5f5f5;
padding: 12px;
border-radius: 8px;
}
二、用媒体查询控制列宽与列数
上面的结构写死了三列,但在手机上应该只剩一列或两列。由于列是由HTML里的.column元素数量决定的,纯Flexbox无法直接减少DOM中的列数。因此实践中常采用两种策略:其一是用媒体查询配合column-count的CSS多列布局,其二是用Flexbox结合JS动态生成列。如果坚持只用Flexbox和媒体查询,可以通过隐藏部分列并让剩余列变宽来实现。
例如默认桌面端显示四列,平板显示两列,手机显示一列。我们可以在HTML中先写四列,然后用媒体查询把多余列display:none,同时调整.column的flex-basis以填满宽度:
.column {
flex: 1 1 25%;
max-width: 25%;
}
@media (max-width: 900px) {
.column:nth-child(3),
.column:nth-child(4) {
display: none;
}
.column {
flex-basis: 50%;
max-width: 50%;
}
}
@media (max-width: 600px) {
.column:nth-child(2) {
display: none;
}
.column {
flex-basis: 100%;
max-width: 100%;
}
}
这种方式的优点是纯CSS可控,缺点是需要提前知道卡片总数并手动分配列,且隐藏列里的卡片不会再流动到可见列中,可能造成分布不均。若对流动性要求高,建议用CSS Grid的grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))配合grid-auto-rows与JS计算行高,但那就超出了Flexbox范畴。
三、Flexbox方案与Grid方案的对比
从维护成本看,Flexbox加媒体查询适合卡片数量固定、列数变化少的后台展示页;Grid适合内容动态加载的信息流。下面的表格列出两者在瀑布流场景下的关键差异:
| 维度 | Flexbox+媒体查询 | CSS Grid |
|---|---|---|
| 是否需固定列DOM | 是 | 否 |
| 响应式列宽控制 | 靠断点切换flex-basis | 靠auto-fill与minmax |
| 卡片流动填充 | 弱,隐藏列后不重排 | 强,自动填充网格 |
| 代码复杂度 | 低,纯样式 | 中,常需少量JS |
如果项目只允许写样式不允许加脚本,且设计稿明确划分了三档断点,那么Flexbox配合媒体查询隐藏列是最省事的做法。需要注意gap在旧版浏览器可能不生效,可用margin替代。另外,卡片内部图片应设width:100%;height:auto防止撑破列宽。
四、完整可运行示例
下面给出一个完整页面示例,包含三列结构与两档断点,复制保存为html文件即可观察效果:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
body { margin: 0; font-family: sans-serif; }
.waterfall {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 12px;
padding: 12px;
}
.column {
flex: 1 1 33.33%;
max-width: 33.33%;
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
background: #eef;
border-radius: 6px;
padding: 10px;
}
.card img { width: 100%; height: auto; }
@media (max-width: 800px) {
.column:nth-child(3) { display: none; }
.column { flex-basis: 50%; max-width: 50%; }
}
@media (max-width: 500px) {
.column:nth-child(2) { display: none; }
.column { flex-basis: 100%; max-width: 100%; }
}
</style>
</head>
<body>
<div class="waterfall">
<div class="column">
<div class="card"><img src="https://picsum.photos/200/300?random=2">短文本</div>
<div class="card"><img src="https://picsum.photos/200/150?random=3">内容</div>
</div>
<div class="column">
<div class="card"><img src="https://picsum.photos/200/250?random=4">说明</div>
<div class="card"><img src="https://picsum.photos/200/200?random=5">示例</div>
</div>
<div class="column">
<div class="card"><img src="https://picsum.photos/200/180?random=6">结尾</div>
</div>
</div>
</body>
</html>
通过以上代码可以看到,Flexbox负责列的横排与卡片的纵排,媒体查询负责在不同宽度下收起列并放大剩余列宽。虽然它不是最灵活的瀑布流,但胜在逻辑直白、无脚本依赖,适合大部分轻量级页面。