Skip to content

响应式布局与容器 ​

响应式布局最常见的投诉不是“不够好看”,而是这些具体问题:

  • 手机打开页面,字小得要放大才能看;
  • 页面能左右晃动,底部总有一条横向滚动条;
  • 全屏弹窗在手机上底部按钮被浏览器工具栏挡住;
  • 某个长标题或长链接一出现,整块布局就被撑坏。

这篇先讲浏览器怎么决定元素的宽度,再给出一套“少写断点”的布局方法,最后逐个修这些坑。

读完这篇你会

  • 写出在大屏不会过宽、小屏不会贴边的内容容器
  • 用自动换行的网格减少断点,用容器查询做组件级响应式
  • 定位并修复横向滚动条、100vh 遮挡、图片溢出等问题

1. 原理:内容像水,容器像杯子 ​

块级元素(div、p、section)默认会撑满父容器的宽度,高度由内容决定。这就像把水倒进杯子:杯子多宽,水面就多宽。所以大部分时候,你不需要给元素写死宽度,它自己就会适应。

响应式布局的思路因此很简单:

  1. 让内容自然流动,宽度用百分比、fr、min() 这类相对单位;
  2. 给容器设一个合理的最大宽度,避免大屏上一行字太长;
  3. 只在布局真的放不下时,用断点调整结构。

写死 width: 1200px 相当于把水冻成冰块,杯子一小就溢出来了,这正是很多横向滚动条的来源。

2. 坑一:手机上字特别小,整个页面被缩小了 ​

现象:页面在电脑上正常,手机打开后像一张缩略图,字小到需要双指放大。

原因:缺少 viewport 设置时,手机浏览器会假装自己有大约 980px 宽,按桌面页面排版,再整体缩小塞进屏幕。

修复:在 <head> 里加上这一行(几乎所有脚手架默认都有,自己写 HTML 时容易漏):

html
<meta name="viewport" content="width=device-width, initial-scale=1" />

怎么避免再犯:新建页面模板时先检查这一行;不要加 user-scalable=no 禁止缩放,视力不好的用户需要放大。

3. 内容容器:控制宽度和边距 ​

css
.page-shell {
  width: min(100% - 32px, 72rem); /* 小屏左右各留 16px,大屏最多 72rem */
  margin-inline: auto;            /* 水平居中 */
}

.article-body {
  max-width: 45rem; /* 正文单独限制行宽,阅读更舒服 */
}

min(A, B) 取两者中较小的值:屏幕窄时 100% - 32px 更小,就用它(自动留出边距);屏幕宽时 72rem 更小,容器就不再变宽。一行代码同时解决了“贴边”和“过宽”两个问题。

正文和页面容器分开设置宽度:表格、卡片列表可以宽一些,大段文字每行保持在 35~45 个汉字左右最易读。

4. 让布局自己换行,减少断点 ​

很多“手机一列、平板两列、电脑三列”的需求,并不需要写三套媒体查询:

css
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: 1rem;
}

这句话的意思是:“每张卡片至少 16rem 宽,能放几列就放几列,剩余空间平分”。容器变窄,列数自动减少。里面的 min(16rem, 100%) 防止在比 16rem 还窄的屏幕上溢出,原理见 Grid 网格布局。

按钮组、标签列表这类一维排列,用 Flex 加 flex-wrap: wrap 就能自动换行:

css
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

5. 断点:在布局失效的地方加,而不是按设备型号加 ​

断点应该加在“内容开始变难看或不可用”的宽度:导航挤不下了、两栏正文太窄了、按钮挤在一起点不准了。打开 DevTools 的设备模式,慢慢拖动宽度,看哪里开始出问题,就在那里加断点。

css
/* 默认写小屏样式(移动优先),宽度足够时再升级为两栏 */
.page-layout {
  display: grid;
  gap: 2rem;
}

@media (min-width: 56rem) {
  .page-layout {
    grid-template-columns: minmax(0, 1fr) 18rem;
  }
}

“移动优先”(默认写小屏,用 min-width 往上加)通常比“桌面优先”(用 max-width 往下改)更好维护,因为小屏布局往往更简单,往上加东西比往下拆东西容易。

容器查询:组件根据自己的宽度变化 ​

同一张卡片,放在宽的主栏里应该横排,放在窄的侧栏里应该竖排。媒体查询只能看整个屏幕宽度,做不到这一点;容器查询可以让组件根据所在容器的宽度调整:

css
.card-slot {
  container-type: inline-size; /* 声明它是一个可被查询的容器 */
}

.card {
  display: grid;
  gap: 0.75rem;
}

@container (min-width: 30rem) {
  .card {
    grid-template-columns: 8rem 1fr; /* 容器够宽时,图片和文字横排 */
  }
}

主流浏览器从 2023 年起都已支持容器查询,做可复用组件时优先考虑它。

6. 坑二:页面能左右晃,底部有横向滚动条 ​

现象:手机上页面可以左右拖动,右边多出一截空白。

原因:某个元素比视口宽。常见元凶:

元凶例子修复
写死的宽度width: 400px改成 max-width: 100% 或相对单位
100vw桌面端 100vw 包含滚动条宽度,比可用宽度多十几像素用 100% 代替
长单词、长链接一串没有空格的 URLoverflow-wrap: anywhere
图片、视频、iframe原始尺寸比屏幕宽max-width: 100%; height: auto
负外边距、绝对定位装饰元素伸出容器父元素 overflow: hidden 或 overflow-x: clip

怎么快速找到元凶:在控制台运行下面这段代码,列出所有右边缘超出视口的元素:

js
const limit = document.documentElement.clientWidth
;[...document.querySelectorAll('body *')]
  .filter((el) => el.getBoundingClientRect().right > limit + 1)
  .forEach((el) => console.log(el))

列表里最深的那一层元素通常就是源头,上面的父元素只是被它撑宽了。

怎么避免再犯:在全局样式里给媒体元素加上保护。不要随手给 html、body 或外层容器加 overflow-x: hidden 来“掩盖”问题:它可能把需要显示的内容裁掉,还会让元素变成滚动容器,导致里面的 position: sticky 失效。确实需要裁剪装饰元素时,优先用不会产生滚动容器的 overflow-x: clip。

css
img,
video,
iframe {
  max-width: 100%;
  height: auto;
}

7. 坑三:100vh 的全屏页面,底部被地址栏挡住 ​

现象:设置了 height: 100vh 的全屏弹窗,在手机浏览器里底部按钮被工具栏遮住,或者页面能多滚出一截。

原因:手机浏览器的地址栏和工具栏会随滚动收起和展开。100vh 按“工具栏收起时”的最大高度计算,工具栏展开时,实际可见区域比它小。

修复:使用动态视口单位 dvh(跟随工具栏变化),并保留 vh 作为老浏览器的回退:

css
.fullscreen {
  height: 100vh;  /* 不支持 dvh 的浏览器使用这一行 */
  height: 100dvh; /* 支持的浏览器会用这一行覆盖上一行 */
}

还有 svh(工具栏展开时的最小高度)和 lvh(工具栏收起时的最大高度),需要高度稳定、不随滚动跳动时用 svh。底部固定按钮还要考虑 iPhone 底部的安全区域,见 移动端适配。

8. 坑四:图片加载后,内容突然往下跳 ​

现象:读着文章,图片加载出来的一瞬间,正在看的文字被挤到了下面。

原因:图片下载完成前,浏览器不知道它有多高,只能先按 0 高度排版;下载完成后再撑开,下面的内容整体下移。这叫布局偏移,会被计入 CLS(累计布局偏移)性能指标。

修复:给图片写上宽高属性,或者用 aspect-ratio 预留比例,浏览器就能提前留出位置:

html
<img src="cover.jpg" width="1200" height="630" alt="封面" />
css
.cover {
  width: 100%;
  height: auto;
  aspect-ratio: 1200 / 630;
  object-fit: cover;
}

9. 宽表格:允许它滚动,而不是压扁 ​

表格硬塞进小屏会变得无法阅读。给它包一层可以横向滚动的容器:

html
<div class="table-scroll">
  <table>...</table>
</div>
css
.table-scroll {
  overflow-x: auto;
}

排查思路 ​

  1. 用 DevTools 的设备模式(Toggle device toolbar)检查 320px、375px、768px,以及它们之间的宽度。很多问题出在“不是标准设备宽度”的中间状态。
  2. 用极端内容测试:超长标题、超长英文单词、空数据、只有一项数据、上百项数据。
  3. 出现横向滚动:运行上面的查找脚本,或者临时给所有元素加 * { outline: 1px solid red; } 观察谁伸出了边界。
  4. 真机复测:dvh、安全区域、工具栏遮挡等问题,模拟器经常复现不了。

检查清单 ​

  • [ ] 页面有 viewport 设置,且没有禁止缩放
  • [ ] 容器用 min() / max-width 控制宽度,正文限制行宽
  • [ ] 卡片列表用 auto-fit + minmax 自动换行
  • [ ] 断点加在布局失效的地方,可复用组件考虑容器查询
  • [ ] 没有写死的宽度和 100vw,媒体元素有 max-width: 100%
  • [ ] 全屏区域使用 dvh 并保留 vh 回退
  • [ ] 图片有宽高或 aspect-ratio,宽表格可横向滚动

延伸阅读 ​

为复用而记录,为理解而整理。