响应式设计
What — 是什么
响应式设计(Responsive Design)让网页根据不同屏幕尺寸自动调整布局和样式,实现一套代码适配多端。
核心原则
- 流体布局(Fluid Grid):使用相对单位(%、vw、rem)代替固定 px,布局随视口自动缩放
- 弹性图片(Flexible Images):图片随容器缩放,不超出父元素宽度
- 媒体查询(Media Queries):根据视口特性应用不同样式
- 容器查询(Container Queries):根据组件容器尺寸而非视口调整样式
视口与设备像素比
viewport 元标签:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
| 属性 | 含义 | 推荐值 |
|---|---|---|
width | 视口宽度 | device-width |
initial-scale | 初始缩放比例 | 1.0 |
maximum-scale | 最大缩放比例 | 5.0(不要设 1.0,影响无障碍) |
user-scalable | 是否允许缩放 | yes(不要设 no,影响无障碍) |
设备像素比(DPR):
| 概念 | 说明 |
|---|---|
| CSS 像素 | 逻辑像素,CSS 使用的单位 |
| 物理像素 | 屏幕实际像素点 |
| DPR | 物理像素 ÷ CSS 像素 |
常见设备 DPR:
iPhone 14:3x(375 CSS px × 3 = 1125 物理像素)
iPad Air:2x
普通桌面显示器:1x
MacBook Retina:2x
viewport 单位:
| 单位 | 含义 | 用途 |
|---|---|---|
vw | 视口宽度的 1% | 流式宽度/字号 |
vh | 视口高度的 1% | 全屏高度 |
vmin | min(vw, vh) | 正方形/居中 |
vmax | max(vw, vh) | 全屏背景 |
dvh | 动态视口高度 | 移动端全屏(排除地址栏) |
svh | 小视口高度 | 移动端最小安全高度 |
lvh | 大视口高度 | 移动端最大可用高度 |
断点策略
移动优先 vs 桌面优先:
| 维度 | 移动优先(推荐) | 桌面优先 |
|---|---|---|
| 基础样式 | 小屏 | 大屏 |
| 媒体查询 | min-width 递增 | max-width 递减 |
| CSS 体积 | 小屏先加载(渐进增强) | 大屏先加载(优雅降级) |
| 移动性能 | 优(基础样式即移动端) | 差(需覆盖桌面样式) |
| 语义 | 从简到繁,符合认知 | 从繁到简,容易遗漏 |
常用断点参考:
| 断点名称 | 宽度范围 | 典型设备 |
|---|---|---|
| xs | 0 - 575px | 小手机 |
| sm | 576 - 767px | 大手机 |
| md | 768 - 991px | 平板竖屏 |
| lg | 992 - 1199px | 平板横屏/小桌面 |
| xl | 1200 - 1399px | 标准桌面 |
| 2xl | 1400px+ | 大屏桌面 |
媒体查询完整语法
/* 基础用法 */
@media (min-width: 768px) { } /* 宽度 ≥ 768px */
@media (max-width: 767px) { } /* 宽度 < 768px */
/* 逻辑运算符 */
@media (min-width: 768px) and (max-width: 1023px) { } /* 且 */
@media (max-width: 767px), (min-width: 1024px) { } /* 或 */
@media not print { } /* 非 */
/* 媒体类型 */
@media screen { } /* 屏幕 */
@media print { } /* 打印 */
@media speech { } /* 语音朗读 */
/* 常用媒体特性 */
@media (orientation: landscape) { } /* 横屏 */
@media (prefers-color-scheme: dark) { } /* 深色模式偏好 */
@media (prefers-reduced-motion: reduce) { } /* 减少动画偏好 */
@media (hover: hover) { } /* 支持悬停(非触摸) */
@media (pointer: fine) { } /* 精确指针(鼠标) */
@media (pointer: coarse) { } /* 粗略指针(触摸) */
@media (resolution: 2dppx) { } /* 高 DPR 设备 */
/* CSS 新语法:范围写法(2023+) */
@media (width >= 768px) { } /* 等价于 min-width: 768px */
@media (768px <= width < 1024px) { } /* 范围 */
容器查询
/* 定义容器 */
.card-wrapper {
container-type: inline-size; /* 按宽度查询 */
container-name: card; /* 命名(可选) */
}
/* 根据容器宽度调整 */
@container card (min-width: 400px) {
.card { display: flex; gap: 16px; }
}
@container card (max-width: 399px) {
.card { display: block; }
}
/* 容器样式查询(实验性) */
@container style(--theme: dark) {
.card { background: #1a1a1a; color: white; }
}
Why — 为什么
适用场景:
- 所有面向多设备的 Web 项目
- 邮件模板(有限支持)
- 组件库设计
- 落地页/营销页
- 后台管理系统
对比替代方案:
| 维度 | 响应式 | 自适应(Adaptive) | 独立移动站 |
|---|---|---|---|
| 开发成本 | 低 | 中 | 高(两套代码) |
| 维护性 | 高 | 中 | 低 |
| 灵活性 | 流式适配 | 固定断点 | 完全定制 |
| URL | 统一 | 统一 | 不同(m.xxx.com) |
| SEO | 好 | 好 | 需配置 rel=alternate |
| 性能 | 中(加载全部资源) | 好(只加载对应断点资源) | 好(针对性优化) |
优缺点:
- ✅ 优点:
- 一套代码,维护成本低
- 流式体验,无固定断点跳跃
- SEO 友好(统一 URL)
- 新设备自动适配
- ❌ 缺点:
- 复杂布局在中间尺寸可能不好看
- 图片资源需做响应式裁切
- 容器查询浏览器支持较新(2023+)
- 移动端可能加载不必要的桌面资源
How — 怎么用
快速上手
/* 移动优先基础样式 */
.card {
padding: 16px;
font-size: 14px;
display: block;
}
/* 平板 */
@media (min-width: 768px) {
.card {
padding: 24px;
font-size: 16px;
}
}
/* 桌面 */
@media (min-width: 1024px) {
.card {
padding: 32px;
font-size: 18px;
display: flex;
}
}
代码示例
1. CSS Grid 响应式布局:
/* auto-fit + minmax 自动响应,无需媒体查询 */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
/* auto-fill vs auto-fit 区别 */
/* auto-fill:创建尽可能多的列轨道,空的也占位 */
/* auto-fit:创建列但折叠空轨道,内容拉伸填满 */
/* 响应式 Grid 命名区域 */
@media (min-width: 768px) {
.layout {
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 250px 1fr;
}
}
2. Flexbox 响应式:
/* flex-wrap 自动换行 */
.flex-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.flex-list > .item {
/* 基础宽度 280px,可伸缩 */
flex: 1 1 280px;
max-width: 100%;
}
/* 导航栏响应式:横排 → 汉堡菜单 */
.nav {
display: flex;
align-items: center;
gap: 16px;
}
.nav__links {
display: flex;
gap: 12px;
}
.nav__toggle {
display: none; /* 桌面端隐藏 */
}
@media (max-width: 767px) {
.nav__links {
display: none; /* 移动端隐藏 */
}
.nav__links.active {
display: flex;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
padding: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.nav__toggle {
display: block;
}
}
3. clamp() 流式排版:
/* 字号在 16px-24px 之间随视口流式变化 */
h1 {
font-size: clamp(1.5rem, 2.5vw + 0.5rem, 2rem);
}
/* 间距流式缩放 */
.container {
padding: clamp(16px, 4vw, 48px);
}
/* 流式宽度 */
.sidebar {
width: clamp(200px, 20vw, 300px);
}
/* clamp 计算公式推导 */
/* clamp(min, preferred, max) */
/* preferred 通常用 vw + 固定偏移:Xvw + Yrem */
/* 确保 Xvw+Yrem >= min 且 Xvw+Yrem <= max */
/* 例:clamp(1rem, 2vw + 0.5rem, 1.5rem) */
/* 视口 375px 时:2%×375 + 8 = 15.5px → 取 max(16, 15.5) = 16px */
/* 视口 1920px 时:2%×1920 + 8 = 46.4px → 取 min(24, 46.4) = 24px */
4. 响应式图片完整方案:
<!-- 方案1:srcset + sizes(分辨率+尺寸适配) -->
<img
src="photo-400.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w,
photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="Photo"
loading="lazy"
decoding="async"
>
<!-- 方案2:picture + art direction(不同尺寸不同裁切) -->
<picture>
<!-- 桌面:宽幅 -->
<source
media="(min-width: 1024px)"
srcset="hero-wide.webp"
type="image/webp"
>
<!-- 平板:中等 -->
<source
media="(min-width: 768px)"
srcset="hero-tablet.webp"
type="image/webp"
>
<!-- 手机:竖版 -->
<source
srcset="hero-mobile.webp"
type="image/webp"
>
<!-- 降级 -->
<img src="hero-mobile.jpg" alt="Hero">
</picture>
<!-- 方案3:DPR 适配(同一尺寸不同分辨率) -->
<img
src="icon-1x.png"
srcset="icon-1x.png 1x, icon-2x.png 2x, icon-3x.png 3x"
alt="Icon"
>
5. 容器查询实战:
/* 卡片组件:在宽容器中横排,窄容器中竖排 */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
gap: 16px;
}
/* 窄容器:竖排 */
@container card (max-width: 399px) {
.card {
grid-template-rows: auto 1fr auto;
}
.card__image {
aspect-ratio: 16/9;
}
}
/* 宽容器:横排 */
@container card (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
grid-template-rows: 1fr auto;
}
.card__image {
grid-row: 1 / 3;
height: 100%;
object-fit: cover;
}
}
/* 超宽容器:大卡片 */
@container card (min-width: 600px) {
.card {
grid-template-columns: 300px 1fr;
}
}
6. CSS 自定义属性响应式切换:
:root {
--content-width: 100%;
--grid-columns: 1;
--font-size-base: 14px;
--spacing: 16px;
}
@media (min-width: 768px) {
:root {
--content-width: 720px;
--grid-columns: 2;
--font-size-base: 16px;
--spacing: 24px;
}
}
@media (min-width: 1024px) {
:root {
--content-width: 960px;
--grid-columns: 3;
--font-size-base: 18px;
--spacing: 32px;
}
}
/* 使用变量,媒体查询只改变量 */
.container { max-width: var(--content-width); margin: 0 auto; }
.grid { display: grid; grid-template-columns: repeat(var(--grid-columns), 1fr); gap: var(--spacing); }
body { font-size: var(--font-size-base); }
7. 深色模式与减少动画偏好:
/* 深色模式 */
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #e5e5e5;
--border: #333;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* 精确指针 vs 粗略指针 */
@media (hover: hover) and (pointer: fine) {
.btn:hover { background: #0056b3; } /* 桌面端悬停效果 */
}
@media (pointer: coarse) {
.btn { min-height: 44px; min-width: 44px; } /* 触摸端最小点击区域 */
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 移动端 1px 边框粗 | CSS 像素 ≠ 设备像素 | transform: scaleY(0.5) 或 border: 0.5px |
| 100vh 包含地址栏 | 移动浏览器 100vh 包含 URL 栏 | 用 100dvh(dynamic viewport height) |
| 横屏布局错乱 | 只考虑了宽度断点 | 加 orientation 媒体查询 |
| 字体过小 | 移动端用了固定 px | 用 rem + 根字体缩放 |
| 图片模糊 | 高 DPR 设备加载 1x 图 | srcset 提供 2x/3x 图 |
| iOS 弹性滚动 | -webkit-overflow-scrolling: touch | overscroll-behavior: contain |
| 安卓键盘弹起 | 视口高度变化导致布局错乱 | 用 visualViewport API 监听 |
| 固定定位在移动端异常 | iOS Safari 的 viewport bug | 避免嵌套 fixed,用 position: sticky |
最佳实践
- 移动优先,
min-width递增 - 断点不用太多:
768px(平板)、1024px(桌面)、1280px(大屏) - 间距用
clamp()流式缩放,减少媒体查询 - 组件级用容器查询,页面级用媒体查询
- 响应式图片用
srcset+sizes+picture - 用 CSS 自定义变量管理断点相关值
- 尊重
prefers-reduced-motion和prefers-color-scheme - 触摸目标最小 44×44px
- 使用
dvh替代vh解决移动端地址栏问题
面试题
Q1: rem、em、vw的区别是什么?
rem 相对根元素字号(html 的 font-size),em 相对父元素字号,vw 相对视口宽度的 1%。rem 适合全局一致的间距和字号,em 适合组件内相对缩放,vw 适合流式响应式布局。实际开发中 rem + clamp() 组合使用最常见。
Q2: 常用的媒体查询断点有哪些?
常用断点:768px(平板竖屏)、1024px(平板横屏/小桌面)、1280px(桌面)。推荐移动优先策略,用 min-width 递增。断点不宜过多,3-4 个即可覆盖主流设备,间距用 clamp() 流式缩放可减少断点需求。
Q3: 移动端1px边框问题如何解决?
高清屏设备像素比(DPR)>1 时,1 个 CSS 像素对应多个物理像素,1px 边框看起来偏粗。解决方案:
transform: scaleY(0.5)缩放、border: 0.5px(部分浏览器支持)、box-shadow 模拟、或使用伪元素绘制细线。
Q4: 100vh在移动端有什么问题?
移动浏览器中 100vh 包含地址栏高度,导致内容被地址栏遮挡。解决方案:使用
100dvh(dynamic viewport height,随地址栏显隐变化)、100svh(small viewport height,取地址栏显示时的最小高度),或用 JS 动态计算可视高度。
Q5: 移动优先和桌面优先有什么区别?推荐哪个?
移动优先先写小屏基础样式,用
min-width递增大屏样式;桌面优先先写大屏样式,用max-width递减小屏样式。推荐移动优先:1) 基础样式即移动端,移动设备无需覆盖样式,性能更好;2) 渐进增强思路更符合可访问性原则;3) CSS 层叠天然适配递增覆盖。桌面优先适合已有桌面站点改造。
Q6: 响应式图片的 srcset 和 sizes 是怎么工作的?
浏览器根据 srcset 中的宽度描述符(如
400w)和 sizes 属性计算当前视口下最合适的图片资源。sizes 告诉浏览器图片在不同断点下的显示宽度(如(max-width: 600px) 100vw, 50vw),浏览器据此选择最接近的图片下载。注意 sizes 不影响显示尺寸,只影响资源选择。需要不同裁切时用<picture>元素。
Q7: CSS 的 clamp() 函数有什么用?
clamp(min, preferred, max)将值限制在 min 和 max 之间,preferred 通常是含 vw 的流式值。常见用途:流式字号font-size: clamp(1rem, 2vw + 0.5rem, 1.5rem),间距自动缩放padding: clamp(16px, 4vw, 48px)。优势:一行代码替代多个媒体查询,中间尺寸平滑过渡无跳跃。计算公式通常为Xvw + Yrem形式。
Q8: 容器查询和媒体查询有什么区别?
媒体查询基于视口尺寸,容器查询基于父容器尺寸。关键区别:1) 媒体查询是全局的,组件样式取决于视口;容器查询是组件级的,同一个组件在不同容器宽度下可以有不同样式;2) 容器查询让组件真正做到”自适应”,不再依赖页面级断点;3) 需要通过
container-type显式声明容器。容器查询更适合组件库开发,媒体查询更适合页面级布局。
相关链接:
- CSS布局Flex与Grid
- [[HTML5语义化]]
- CSS容器查询
- 移动端适配方案
- CSS新特性与现代布局