1. CSS 多列瀑布流
多列布局实现视觉瀑布流;内容按列填充,不保证按行从左到右。
.masonry {
column-count: 3;
column-gap: 12px;
}
.card {
break-inside: avoid;
margin-block-end: 12px;
}Appearance
如何实现瀑布流、水平垂直居中、两栏/三栏布局、上下固定中间滚动、圣杯布局和双飞翼布局?这些题在现代项目里应该优先用什么方案,哪些旧方案只需要理解原理?
纯 CSS 常见方案是多列布局:
.masonry {
column-count: 3;
column-gap: 16px;
}
.card {
break-inside: avoid;
margin-block-end: 16px;
}这个方案适合图片墙、卡片流、资料列表等“视觉上像瀑布”的场景。column-count 负责把内容分成多列,break-inside: avoid 尽量避免单张卡片被拆到两列。
它的边界也要说明:多列布局是按列填充,不是按行从左到右填充;如果业务要求“第 1、2、3 张必须在第一行从左到右出现”,多列布局就不合适。需要严格顺序、虚拟列表、动态测量或拖拽排序时,通常用 JavaScript 计算列高,或使用成熟组件。
居中题不要直接背一个万能答案,先判断“居中的对象”和“是否已知尺寸”:
实际项目里要先分清楚两类父元素:
body 或指定容器下,再用 position: fixed; inset: 0 形成遮罩层。此时“父元素”不是业务组件最近的 DOM 父级,而是框架创建的浮层容器。所以,Flex/Grid 通常不是加在 body 或整个应用根节点上,而是加在“负责这次居中关系的容器”上。这个容器可能是局部 wrapper,也可能是全局遮罩层。
判断顺序可以这样记:
text-align、line-height。margin-inline: auto。body 或指定容器,并用 position: fixed 覆盖视口。position: relative,子元素 position: absolute。| # | 方案 | 适用场景 | 边界 |
|---|---|---|---|
| 1 | text-align: center | 行内内容、文本、inline-block 子元素水平居中 | 只影响行内级内容,不会让普通块级元素自己居中。 |
| 2 | line-height 等于容器高度 | 单行文本垂直居中 | 只适合单行;多行文本会互相挤压。 |
| 3 | 定宽块 + margin-inline: auto | 块级元素水平居中 | 元素需要有可计算宽度。 |
| 4 | Flex:justify-content: center; align-items: center | 常规水平垂直居中 | 现代项目首选,适合容器内一个或多个项目。 |
| 5 | Grid:place-items: center | 单个项目或二维区域内居中 | 写法短,适合弹窗壳、空状态、图标按钮等。 |
| 6 | 绝对定位 + transform | 未知宽高的绝对定位元素居中 | 会创建变换上下文,极少数场景可能出现亚像素渲染。 |
| 7 | 绝对定位 + 负 margin | 已知宽高的绝对定位元素居中 | 必须知道元素宽高,尺寸变化就要同步改负 margin。 |
| 8 | 绝对定位 + inset: 0; margin: auto | 已知宽高的绝对定位元素居中 | 需要明确宽高,并且父元素要能形成定位包含块。 |
| 9 | display: table-cell; vertical-align: middle | 旧项目、多行内容垂直居中 | 视觉布局可用,但不应为了布局滥用语义表格。 |
| 10 | inline-block + 伪元素撑满高度 + vertical-align: middle | 兼容旧环境的未知高度内容垂直居中 | 写法绕,现代项目通常用 Flex/Grid 替代。 |
现代项目最常用的是 Flex、Grid、绝对定位加 transform。下面这些历史方案在面试里仍然可能出现,要能说清楚它们为什么成立:
/* 1. 单行文本水平 + 垂直居中 */
.text-center {
height: 48px;
line-height: 48px;
text-align: center;
}
/* 2. 块级元素水平居中 */
.block-center {
width: 320px;
margin-inline: auto;
}
/* 3. Flex 水平垂直居中 */
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
/* 4. Grid 水平垂直居中 */
.grid-center {
display: grid;
place-items: center;
}
/* 5. 未知宽高:绝对定位 + transform */
.absolute-transform-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 6. 已知宽高:绝对定位 + 负 margin */
.absolute-negative-margin-center {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 120px;
margin-left: -100px;
margin-top: -60px;
}
/* 7. 已知宽高:四边为 0 + auto margin */
.absolute-inset-center {
position: absolute;
inset: 0;
width: 200px;
height: 120px;
margin: auto;
}
/* 8. table-cell 垂直居中 */
.table-cell-center {
display: table-cell;
width: 320px;
height: 180px;
text-align: center;
vertical-align: middle;
}
.table-cell-center > .content {
display: inline-block;
}
/* 9. inline-block + 伪元素辅助垂直居中 */
.pseudo-middle {
height: 180px;
text-align: center;
}
.pseudo-middle::before {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
}
.pseudo-middle > .content {
display: inline-block;
vertical-align: middle;
}如果题目问“浮动元素怎么居中”,可以回答历史写法:给浮动元素一个明确宽度,再通过相对定位、left: 50% 和负 margin 拉回一半宽度。但这属于旧布局技巧,现代项目不应为了居中引入 float。
左侧固定、右侧自适应:
.layout {
display: flex;
}
.aside {
flex: 0 0 240px;
}
.main {
min-width: 0;
flex: 1;
}同一道「左固定右自适应」题,面试常要求说出多种实现,理解每种成立的原理:
/* 1. Flex:最常用,右栏 flex: 1 吃掉剩余空间 */
.flex .main { min-width: 0; flex: 1; }
/* 2. Grid:左列定宽,右列 minmax(0, 1fr) */
.grid {
display: grid;
grid-template-columns: 200px minmax(0, 1fr);
}
/* 3. float + BFC:左栏浮动,右栏 overflow 触发 BFC 不与浮动重叠 */
.float .aside { float: left; width: 200px; }
.float .main { overflow: hidden; }
/* 4. 绝对定位:左栏 absolute,右栏 margin-left 留出空间 */
.absolute { position: relative; }
.absolute .aside { position: absolute; left: 0; width: 200px; }
.absolute .main { margin-left: 200px; }
/* 5. table:容器 table,两栏 table-cell,右栏自动撑满 */
.table { display: table; width: 100%; }
.table .aside { display: table-cell; width: 200px; }
.table .main { display: table-cell; }现代项目优先 Flex 或 Grid;float + BFC、绝对定位、table 属于历史方案,了解原理即可。
两边固定、中间自适应:
现代项目优先使用 Grid 或 Flex。下面两种写法复用同一份 HTML,只需切换容器上的修饰类:
<div class="three-column three-column--grid">
<aside class="left">左栏</aside>
<main class="main">中间内容</main>
<aside class="right">右栏</aside>
</div>.three-column--grid {
display: grid;
grid-template-columns: 200px minmax(0, 1fr) 200px;
gap: 16px;
}
.three-column--flex {
display: flex;
gap: 16px;
}
.three-column--flex > .left,
.three-column--flex > .right {
flex: 0 0 200px;
}
.three-column--flex > .main {
min-width: 0;
flex: 1;
}minmax(0, 1fr) 比简单的 auto 更稳:中间列会吃掉剩余空间,并允许长内容在空间不足时收缩。Flex 更适合一维排列,Grid 对三栏页面骨架更直观。
绝对定位也能实现,但容器需要明确最小高度,内容高度和侧栏定位的维护成本更高:
<div class="three-column-absolute">
<aside class="left">左栏</aside>
<main class="main">中间内容</main>
<aside class="right">右栏</aside>
</div>.three-column-absolute {
position: relative;
min-height: 12rem;
}
.three-column-absolute > .left,
.three-column-absolute > .right {
position: absolute;
inset-block: 0;
width: 200px;
}
.three-column-absolute > .left { inset-inline-start: 0; }
.three-column-absolute > .right { inset-inline-end: 0; }
.three-column-absolute > .main {
min-width: 0;
margin-inline: 200px;
}浮动方案需要让左右栏先于主栏出现,再由主栏的左右 margin 预留空间:
<div class="three-column-float">
<aside class="left">左栏</aside>
<aside class="right">右栏</aside>
<main class="main">中间内容</main>
</div>.three-column-float {
display: flow-root;
}
.three-column-float > .left {
float: left;
width: 200px;
}
.three-column-float > .right {
float: right;
width: 200px;
}
.three-column-float > .main {
min-width: 0;
margin-inline: 200px;
}这两种都是有效的历史方案,但绝对定位会让侧栏脱离正常流,浮动方案又受 DOM 顺序和清除浮动影响。现代业务布局一般不以它们为首选。
页面或面板常见结构是头部固定高度、底部固定高度,中间内容区域填满并滚动:
.shell {
min-height: 100dvh;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
}
.content {
min-height: 0;
overflow: auto;
}关键是中间行写成 minmax(0, 1fr),滚动容器写 min-height: 0; overflow: auto。否则内容可能把容器撑开,导致滚动发生在整个页面,而不是中间区域。
圣杯布局和双飞翼布局都是旧时代的三栏浮动方案,目标是:
区别:
| 布局 | 预留侧栏空间的位置 | 侧栏归位方式 | DOM 额外结构 |
|---|---|---|---|
| 圣杯布局 | 在外层容器上用左右 padding 预留空间 | 左右栏用负 margin 加 position: relative 位移 | 不需要额外包一层主内容 |
| 双飞翼布局 | 在主内容内部用左右 margin 预留空间 | 左右栏主要用负 margin 归位 | 主内容通常多包一层 |
现代实现已经在上一节给出;下面保留完整浮动源码,用于说明两种经典布局为什么不同。
圣杯布局:容器 padding 预留侧栏空间。
<div class="holy-grail">
<main class="holy-grail__main">中间内容</main>
<aside class="holy-grail__left">左栏</aside>
<aside class="holy-grail__right">右栏</aside>
</div>.holy-grail {
padding-inline: 200px;
}
.holy-grail::after {
content: "";
display: block;
clear: both;
}
.holy-grail__main,
.holy-grail__left,
.holy-grail__right {
float: left;
}
.holy-grail__main {
width: 100%;
}
.holy-grail__left {
position: relative;
left: -200px;
width: 200px;
margin-left: -100%;
}
.holy-grail__right {
position: relative;
right: -200px;
width: 200px;
margin-left: -200px;
}双飞翼布局:主内容内部 margin 预留侧栏空间。
<div class="double-wing">
<main class="double-wing__main">
<div class="double-wing__content">中间内容</div>
</main>
<aside class="double-wing__left">左栏</aside>
<aside class="double-wing__right">右栏</aside>
</div>.double-wing::after {
content: "";
display: block;
clear: both;
}
.double-wing__main,
.double-wing__left,
.double-wing__right {
float: left;
}
.double-wing__main {
width: 100%;
}
.double-wing__content {
margin-inline: 200px;
}
.double-wing__left {
width: 200px;
margin-left: -100%;
}
.double-wing__right {
width: 200px;
margin-left: -200px;
}两者都让主内容在 DOM 中靠前,但维护负 margin 和固定侧栏宽度比较脆弱。如果题目只要求实现三栏布局,优先选择上一节的 Flex 或 Grid;只有明确考察经典布局时才使用这些浮动写法。
等高列不必再用很重的 hack。常见方案:
align-items: stretch,适合卡片列表等高列。"品"字布局:上方一个元素居中,下方两个元素并排居中。
Flex 实现(推荐):
<div class="pin">
<div class="pin__top">1</div>
<div class="pin__row">
<div>2</div>
<div>3</div>
</div>
</div>.pin {
display: flex;
flex-direction: column;
align-items: center;
}
.pin__top {
width: 100px;
height: 100px;
}
.pin__row {
display: flex;
gap: 0;
}
.pin__row > div {
width: 100px;
height: 100px;
}浮动实现(旧方案,了解原理):
<div class="pin-float">
<div class="pin-float__top">1</div>
<div class="pin-float__row">
<div>2</div>
<div>3</div>
</div>
</div>.pin-float__top {
width: 100px;
height: 100px;
margin-inline: auto;
}
.pin-float__row {
display: flow-root;
width: 200px;
margin-inline: auto;
}
.pin-float__row > div {
float: left;
width: 100px;
height: 100px;
}inline-block 实现(旧方案,注意源码空白):
<div class="pin-inline">
<div class="pin-inline__top">1</div>
<div class="pin-inline__row">
<div>2</div>
<div>3</div>
</div>
</div>.pin-inline {
text-align: center;
}
.pin-inline__top {
width: 100px;
height: 100px;
margin-inline: auto;
}
.pin-inline__row {
font-size: 0;
}
.pin-inline__row > div {
display: inline-block;
width: 100px;
height: 100px;
font-size: 1rem;
vertical-align: top;
}font-size: 0 用来消除两个 inline-block 之间由换行和空格产生的间隙;子元素需要恢复正常字号。
Grid 实现(推荐):
<div class="grid9">
<div>1</div><div>2</div><div>3</div>
<div>4</div><div>5</div><div>6</div>
<div>7</div><div>8</div><div>9</div>
</div>.grid9 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.grid9 > div {
aspect-ratio: 1;
background: skyblue;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
}Flex 实现:
.grid9 {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.grid9 > div {
/* (100% - 2*gap) / 3,这里用固定宽度示例 */
width: calc((100% - 16px) / 3);
aspect-ratio: 1;
background: skyblue;
}Float 实现(历史方案):
<div class="grid9-float">
<div>1</div><div>2</div><div>3</div>
<div>4</div><div>5</div><div>6</div>
<div>7</div><div>8</div><div>9</div>
</div>.grid9-float {
display: flow-root;
width: 300px;
}
.grid9-float > div {
float: left;
box-sizing: border-box;
width: 100px;
height: 100px;
border: 1px solid CanvasText;
}inline-block 实现(历史方案):
<div class="grid9-inline">
<div>1</div><div>2</div><div>3</div>
<div>4</div><div>5</div><div>6</div>
<div>7</div><div>8</div><div>9</div>
</div>.grid9-inline {
width: 300px;
font-size: 0;
}
.grid9-inline > div {
display: inline-block;
box-sizing: border-box;
width: 100px;
height: 100px;
border: 1px solid CanvasText;
font-size: 1rem;
vertical-align: top;
}Table 布局实现(历史方案):
<div class="grid9-table">
<div class="grid9-table__row">
<div>1</div><div>2</div><div>3</div>
</div>
<div class="grid9-table__row">
<div>4</div><div>5</div><div>6</div>
</div>
<div class="grid9-table__row">
<div>7</div><div>8</div><div>9</div>
</div>
</div>.grid9-table {
display: table;
border-spacing: 0;
}
.grid9-table__row {
display: table-row;
}
.grid9-table__row > div {
display: table-cell;
box-sizing: border-box;
width: 100px;
height: 100px;
border: 1px solid CanvasText;
vertical-align: middle;
text-align: center;
}Grid 直接表达三行三列,语义和维护成本最低;Flex 次之。Float、inline-block 和 table 布局可以解释历史实现原理,但不应为了兼容已经不再支持的环境而成为新项目首选。
多列布局实现视觉瀑布流;内容按列填充,不保证按行从左到右。
.masonry {
column-count: 3;
column-gap: 12px;
}
.card {
break-inside: avoid;
margin-block-end: 12px;
}全屏浮层负责定位和遮罩,弹框本体负责内容布局。
Dialog 通常挂到 body 或指定容器下;预览中用局部舞台模拟 fixed 全局浮层。
<!-- Dialog 通常由框架挂到 body 或指定容器 -->
<main class="app">...</main>
<div class="dialog-layer" aria-label="全局弹框层">
<div class="dialog-mask" aria-hidden="true"></div>
<section
class="dialog"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
>
<header class="dialog-header">
<h2 id="dialog-title">确认提交</h2>
<button aria-label="关闭">×</button>
</header>
<div class="dialog-body">...</div>
<footer class="dialog-footer">
<button>取消</button>
<button class="primary">确认</button>
</footer>
</section>
</div>
.dialog-layer {
position: fixed;
inset: 0;
z-index: 2000;
display: grid;
place-items: center;
padding: 24px;
}
.dialog-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.45);
}
.dialog {
position: relative;
z-index: 1;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
inline-size: min(520px, 100%);
max-block-size: min(640px, calc(100vh - 48px));
overflow: hidden;
border-radius: 8px;
background: #fff;
}
.dialog-header,
.dialog-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
}
.dialog-body {
min-height: 0;
padding: 20px;
overflow: auto;
}页面骨架优先 Grid;中间列用 `minmax(0, 1fr)` 处理长内容收缩。
.layout {
display: grid;
grid-template-columns:
160px minmax(0, 1fr) 160px;
gap: 12px;
}
.main {
min-width: 0;
}滚动内容 1
滚动内容 2
滚动内容 3
滚动内容 4
滚动内容 5
滚动内容 6
滚动内容 7
滚动内容 8
header/footer 在同一个滚动容器里用 sticky 吸顶和吸底。
.shell {
block-size: 320px;
overflow: auto;
}
.shell-header {
position: sticky;
top: 0;
z-index: 1;
}
.shell-footer {
position: sticky;
bottom: 0;
z-index: 1;
}
.shell-content {
padding: 12px;
}.layout-case__layout-preview--holy外层 padding 预留侧栏,左右栏用负 margin 和 relative 位移归位。
圣杯和双飞翼用于理解旧浮动题;新项目优先 Flex 或 Grid。
<div class="layout-case__layout-preview layout-case__layout-preview--holy">
<main class="layout-case__layout-main">main first</main>
<aside class="layout-case__layout-left">left</aside>
<aside class="layout-case__layout-right">right</aside>
</div>
/* 圣杯布局:左侧预览的实际实现 */
.layout-case__layout-preview {
--layout-side: 72px;
}
.layout-case__layout-preview--holy {
display: flow-root;
padding-inline: var(--layout-side);
}
.layout-case__layout-preview--holy .layout-case__layout-main,
.layout-case__layout-preview--holy .layout-case__layout-left,
.layout-case__layout-preview--holy .layout-case__layout-right {
float: left;
}
.layout-case__layout-preview--holy .layout-case__layout-main {
width: 100%;
}
.layout-case__layout-preview--holy .layout-case__layout-left {
width: var(--layout-side);
margin-left: -100%;
position: relative;
left: calc(-1 * var(--layout-side));
}
.layout-case__layout-preview--holy .layout-case__layout-right {
width: var(--layout-side);
margin-left: calc(-1 * var(--layout-side));
position: relative;
right: calc(-1 * var(--layout-side));
}