Skip to content

瀑布流、居中和经典布局案例 ​

问题 ​

如何实现瀑布流、水平垂直居中、两栏/三栏布局、上下固定中间滚动、圣杯布局和双飞翼布局?这些题在现代项目里应该优先用什么方案,哪些旧方案只需要理解原理?

结论 ​

理解路径 ​

  1. 经典布局题先判断布局维度:一维分布优先 Flex,二维骨架优先 Grid,文本或卡片流式排布才考虑多列布局。
  2. 现代项目不要默认使用远程 iframe 示例、浮动 hack 或表格布局;它们可以作为历史方案理解,但主答案应能落到可维护的本地 CSS。
  3. 居中题要先问“居中对象是什么”:单行文本、块级盒子、未知宽高弹窗、绝对定位元素、浮动元素,对应答案不同。
  4. 圣杯布局和双飞翼布局的本质是“中间内容优先渲染 + 两侧固定 + 中间自适应”的旧浮动方案;现在实现同类布局优先 Flex 或 Grid。
  5. 瀑布流要说明视觉效果和顺序代价:CSS 多列布局简单,但按列填充;严格按行顺序和精确高度排布通常需要 JavaScript 或专门布局算法。

瀑布流怎么答 ​

纯 CSS 常见方案是多列布局:

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 计算列高,或使用成熟组件。

居中怎么答 ​

居中题不要直接背一个万能答案,先判断“居中的对象”和“是否已知尺寸”:

实际项目里要先分清楚两类父元素:

  • 局部居中:例如空状态、卡片图标、表格局部 loading,通常复用当前模块里最近的局部容器;如果这个容器还承担列表、表单、页面骨架等职责,就新建一层很薄的 wrapper 专门承载居中关系。
  • 全局浮层:例如 Dialog、Drawer、MessageBox、全屏 Loading,通常由 UI 框架通过 Portal/Teleport 挂到 body 或指定容器下,再用 position: fixed; inset: 0 形成遮罩层。此时“父元素”不是业务组件最近的 DOM 父级,而是框架创建的浮层容器。

所以,Flex/Grid 通常不是加在 body 或整个应用根节点上,而是加在“负责这次居中关系的容器”上。这个容器可能是局部 wrapper,也可能是全局遮罩层。

判断顺序可以这样记:

  1. 只是文本或行内内容:优先 text-align、line-height。
  2. 只是块级元素水平居中:优先固定/约束宽度 + margin-inline: auto。
  3. 需要把一个子元素放到模块区域正中:给最近的局部容器设置 Flex 或 Grid。
  4. 元素需要脱离普通文档流,例如弹窗、抽屉、全屏浮层:通常挂到 body 或指定容器,并用 position: fixed 覆盖视口。
  5. 元素只需要相对某个卡片、按钮或局部面板定位,例如角标、tooltip 箭头:才让最近的业务父元素 position: relative,子元素 position: absolute。
  6. 不要为了居中把页面根布局、列表容器、表单容器强行改成 Grid/Flex;那会影响它下面所有直接子元素的布局语义。
#方案适用场景边界
1text-align: center行内内容、文本、inline-block 子元素水平居中只影响行内级内容,不会让普通块级元素自己居中。
2line-height 等于容器高度单行文本垂直居中只适合单行;多行文本会互相挤压。
3定宽块 + margin-inline: auto块级元素水平居中元素需要有可计算宽度。
4Flex:justify-content: center; align-items: center常规水平垂直居中现代项目首选,适合容器内一个或多个项目。
5Grid:place-items: center单个项目或二维区域内居中写法短,适合弹窗壳、空状态、图标按钮等。
6绝对定位 + transform未知宽高的绝对定位元素居中会创建变换上下文,极少数场景可能出现亚像素渲染。
7绝对定位 + 负 margin已知宽高的绝对定位元素居中必须知道元素宽高,尺寸变化就要同步改负 margin。
8绝对定位 + inset: 0; margin: auto已知宽高的绝对定位元素居中需要明确宽高,并且父元素要能形成定位包含块。
9display: table-cell; vertical-align: middle旧项目、多行内容垂直居中视觉布局可用,但不应为了布局滥用语义表格。
10inline-block + 伪元素撑满高度 + vertical-align: middle兼容旧环境的未知高度内容垂直居中写法绕,现代项目通常用 Flex/Grid 替代。

现代项目最常用的是 Flex、Grid、绝对定位加 transform。下面这些历史方案在面试里仍然可能出现,要能说清楚它们为什么成立:

css
/* 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。

两栏和三栏布局怎么答 ​

左侧固定、右侧自适应:

css
.layout {
  display: flex;
}

.aside {
  flex: 0 0 240px;
}

.main {
  min-width: 0;
  flex: 1;
}

同一道「左固定右自适应」题,面试常要求说出多种实现,理解每种成立的原理:

css
/* 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,只需切换容器上的修饰类:

html
<div class="three-column three-column--grid">
  <aside class="left">左栏</aside>
  <main class="main">中间内容</main>
  <aside class="right">右栏</aside>
</div>
css
.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 对三栏页面骨架更直观。

绝对定位也能实现,但容器需要明确最小高度,内容高度和侧栏定位的维护成本更高:

html
<div class="three-column-absolute">
  <aside class="left">左栏</aside>
  <main class="main">中间内容</main>
  <aside class="right">右栏</aside>
</div>
css
.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 预留空间:

html
<div class="three-column-float">
  <aside class="left">左栏</aside>
  <aside class="right">右栏</aside>
  <main class="main">中间内容</main>
</div>
css
.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 顺序和清除浮动影响。现代业务布局一般不以它们为首选。

上下固定、中间滚动怎么答 ​

页面或面板常见结构是头部固定高度、底部固定高度,中间内容区域填满并滚动:

css
.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 中主内容写在前面,优先渲染。
  • 左右两栏固定宽度,中间栏自适应。
  • 通过浮动、负 margin 和额外位移把三栏摆回视觉位置。

区别:

布局预留侧栏空间的位置侧栏归位方式DOM 额外结构
圣杯布局在外层容器上用左右 padding 预留空间左右栏用负 margin 加 position: relative 位移不需要额外包一层主内容
双飞翼布局在主内容内部用左右 margin 预留空间左右栏主要用负 margin 归位主内容通常多包一层

现代实现已经在上一节给出;下面保留完整浮动源码,用于说明两种经典布局为什么不同。

圣杯布局:容器 padding 预留侧栏空间。

html
<div class="holy-grail">
  <main class="holy-grail__main">中间内容</main>
  <aside class="holy-grail__left">左栏</aside>
  <aside class="holy-grail__right">右栏</aside>
</div>
css
.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 预留侧栏空间。

html
<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>
css
.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。常见方案:

  • Flex 同一行默认 align-items: stretch,适合卡片列表等高列。
  • Grid 同一行网格项也能天然拉伸到同一行高度。
  • 表格布局能等高,但不应为了布局滥用语义表格。
  • 旧方案里的超大 padding + 负 margin、假背景图等只需要知道历史原因,不建议作为现代首选。

实现"品"字布局 ​

"品"字布局:上方一个元素居中,下方两个元素并排居中。

Flex 实现(推荐):

html
<div class="pin">
  <div class="pin__top">1</div>
  <div class="pin__row">
    <div>2</div>
    <div>3</div>
  </div>
</div>
css
.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;
}

浮动实现(旧方案,了解原理):

html
<div class="pin-float">
  <div class="pin-float__top">1</div>
  <div class="pin-float__row">
    <div>2</div>
    <div>3</div>
  </div>
</div>
css
.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 实现(旧方案,注意源码空白):

html
<div class="pin-inline">
  <div class="pin-inline__top">1</div>
  <div class="pin-inline__row">
    <div>2</div>
    <div>3</div>
  </div>
</div>
css
.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 实现(推荐):

html
<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>
css
.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 实现:

css
.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 实现(历史方案):

html
<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>
css
.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 实现(历史方案):

html
<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>
css
.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 布局实现(历史方案):

html
<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>
css
.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 布局可以解释历史实现原理,但不应为了兼容已经不再支持的环境而成为新项目首选。

Demo ​

1. CSS 多列瀑布流

A
B
C
D
E
F
G

多列布局实现视觉瀑布流;内容按列填充,不保证按行从左到右。

.masonry {
  column-count: 3;
  column-gap: 12px;
}

.card {
  break-inside: avoid;
  margin-block-end: 12px;
}

2. 全局浮层 Dialog 居中

页面内容业务页面不负责弹框居中

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;
}

3. 两侧固定,中间自适应

main: minmax(0, 1fr)

页面骨架优先 Grid;中间列用 `minmax(0, 1fr)` 处理长内容收缩。

.layout {
  display: grid;
  grid-template-columns:
    160px minmax(0, 1fr) 160px;
  gap: 12px;
}

.main {
  min-width: 0;
}

4. 滚动容器内 Sticky 头尾

Header sticky top

滚动内容 1

滚动内容 2

滚动内容 3

滚动内容 4

滚动内容 5

滚动内容 6

滚动内容 7

滚动内容 8

Footer sticky bottom

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;
}

5. 圣杯布局和双飞翼布局

圣杯布局

.layout-case__layout-preview--holy
main first

外层 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));
}

参考来源 ​