主题
响应式设计
什么是响应式设计
响应式设计(Responsive Web Design,RWD)是一种让网页在不同设备和屏幕尺寸下都能提供良好体验的设计方法。它的核心理念是同一份代码,多种呈现——而不是为每种设备写一套独立的页面。
Ethan Marcotte 在 2010 年首次提出这个概念,定义了响应式设计的三大技术支柱:
- 流式网格布局(Fluid Grids)—— 使用百分比而非固定像素
- 弹性图片/媒体(Flexible Images)—— 图片随容器缩放
- 媒体查询(Media Queries)—— 根据设备特征应用不同样式
到了 2025 年,响应式的技术手段已经远超这三点——容器查询、clamp()、现代 CSS 布局(Flex/Grid)等新工具让响应式设计变得更加优雅。
Viewport 视口
视口基础
移动端浏览器有两个视口概念:
- 布局视口(Layout Viewport):网页的实际渲染宽度,默认通常是 980px(模拟桌面宽度)
- 视觉视口(Visual Viewport):用户实际看到的区域,即屏幕宽度
如果不做任何设置,移动端浏览器会把 980px 宽的页面缩小到屏幕内,导致文字极小、无法阅读。
viewport meta 标签
html
<meta name="viewport" content="width=device-width, initial-scale=1.0">| 属性 | 说明 | 常用值 |
|---|---|---|
width | 布局视口宽度 | device-width(与设备宽度一致) |
initial-scale | 初始缩放比例 | 1.0(不缩放) |
maximum-scale | 最大缩放比例 | 1.0(禁止放大,但影响无障碍) |
user-scalable | 是否允许用户缩放 | yes(推荐保持 yes) |
最佳实践:
html
<!-- ✅ 推荐 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- ❌ 不推荐:禁止缩放影响无障碍访问 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">viewport 单位
| 单位 | 含义 | 说明 |
|---|---|---|
vw | 视口宽度的 1% | 100vw = 视口宽度 |
vh | 视口高度的 1% | 100vh = 视口高度 |
vmin | vw 和 vh 中较小的那个 | 适合正方形布局 |
vmax | vw 和 vh 中较大的那个 | |
dvh | 动态视口高度 | 手机浏览器地址栏收起/展开时动态变化 |
svh | 最小视口高度 | 地址栏展开时的高度 |
lvh | 最大视口高度 | 地址栏收起时的高度 |
移动端 100vh 的坑:
在移动端浏览器(如 iOS Safari)中,100vh 等于浏览器的最大视口高度(包含地址栏隐藏后的空间),而非当前可见区域。这会导致底部内容被地址栏遮挡。
css
/* ❌ 问题写法 */
.full-screen {
height: 100vh; /* 在移动端可能比可见区域高 */
}
/* ✅ 现代解决方案 */
.full-screen {
height: 100dvh; /* 动态视口高度,随地址栏变化 */
}
/* ✅ 向下兼容方案 */
.full-screen {
height: 100vh;
height: 100dvh; /* 支持的浏览器会使用 dvh */
}媒体查询(Media Queries)
基本语法
css
@media media-type and (condition) {
/* 样式规则 */
}css
@media screen and (max-width: 768px) {
.sidebar {
display: none;
}
}
@media screen and (min-width: 769px) and (max-width: 1024px) {
.container {
max-width: 960px;
}
}常用断点
业界没有统一标准,但以下断点被广泛使用:
css
/* 手机(竖屏) */
@media (max-width: 480px) { }
/* 手机(横屏)/ 小平板 */
@media (min-width: 481px) and (max-width: 768px) { }
/* 平板 */
@media (min-width: 769px) and (max-width: 1024px) { }
/* 桌面 */
@media (min-width: 1025px) and (max-width: 1440px) { }
/* 大屏 */
@media (min-width: 1441px) { }Tailwind CSS 的默认断点(也是目前最流行的参考方案):
| 前缀 | 最小宽度 | 典型设备 |
|---|---|---|
sm | 640px | 大手机(横屏) |
md | 768px | 平板 |
lg | 1024px | 笔记本 |
xl | 1280px | 桌面显示器 |
2xl | 1536px | 大屏显示器 |
Mobile First vs Desktop First
Mobile First(移动优先):默认样式为移动端,用 min-width 向上适配。
css
.container {
padding: 16px; /* 移动端默认 */
}
@media (min-width: 768px) {
.container {
padding: 24px; /* 平板 */
}
}
@media (min-width: 1024px) {
.container {
padding: 32px; /* 桌面 */
}
}Desktop First(桌面优先):默认样式为桌面端,用 max-width 向下适配。
css
.container {
padding: 32px; /* 桌面默认 */
}
@media (max-width: 1024px) {
.container {
padding: 24px;
}
}
@media (max-width: 768px) {
.container {
padding: 16px;
}
}推荐 Mobile First,原因:
- 移动端用户占比超过 50%,优先保证移动体验
- 从简单到复杂的渐进增强更容易维护
- 移动端样式更简单(通常单列),作为基础更合理
- 性能更优——移动设备不需要下载和解析桌面端的额外样式
媒体特性进阶
除了宽度,媒体查询还支持更多条件:
css
/* 暗色模式 */
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #f0f0f0;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* 高分辨率屏幕 */
@media (min-resolution: 2dppx) {
.logo {
background-image: url('logo@2x.png');
}
}
/* 悬停能力检测 */
@media (hover: hover) {
.button:hover {
background: #0066cc; /* 只在支持悬停的设备上应用 */
}
}
/* 指针精度检测 */
@media (pointer: coarse) {
.button {
min-height: 44px; /* 触摸设备使用更大的点击区域 */
min-width: 44px;
}
}
/* 横屏/竖屏 */
@media (orientation: landscape) { }
@media (orientation: portrait) { }hover: hover 的重要性:在触摸设备上,:hover 状态会在点击后"粘住",带来奇怪的 UI 行为。用 @media (hover: hover) 包裹 hover 样式可以避免这个问题。
范围语法(Media Queries Level 4)
CSS 媒体查询现在支持更直观的范围语法:
css
/* 旧语法 */
@media (min-width: 768px) and (max-width: 1024px) { }
/* 新语法(范围写法) */
@media (768px <= width <= 1024px) { }
/* 更多范围写法 */
@media (width >= 768px) { }
@media (width < 1024px) { }容器查询(Container Queries)
为什么需要容器查询
媒体查询基于视口宽度,但组件的实际可用空间取决于它在页面中的位置——侧边栏中的卡片和主内容区的卡片,即使视口宽度相同,可用空间也完全不同。
视口宽度 1200px
┌──────────┬──────────────────────────┐
│ Sidebar │ Main Content │
│ 300px │ 900px │
│ │ │
│ ┌──────┐ │ ┌──────┐ ┌──────┐ │
│ │ Card │ │ │ Card │ │ Card │ │
│ │ 260px│ │ │ 420px│ │ 420px│ │
│ └──────┘ │ └──────┘ └──────┘ │
└──────────┴──────────────────────────┘
同样的 Card 组件,在 sidebar 中只有 260px,
在 main 中有 420px。媒体查询无法区分这两种情况。容器查询解决了这个问题——让组件根据自身容器的尺寸而非视口尺寸来调整样式。
基本用法
css
/* 步骤一:声明容器 */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* 步骤二:编写容器查询 */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}容器 ≥ 400px 时(横向排列): 容器 < 400px 时(纵向排列):
┌────────────────────────┐ ┌──────────┐
│ ┌──────┐ ┌───────────┐ │ │ ┌──────┐ │
│ │ Img │ │ Title │ │ │ │ Img │ │
│ │ │ │ Content │ │ │ └──────┘ │
│ └──────┘ └───────────┘ │ │ Title │
└────────────────────────┘ │ Content │
└──────────┘container-type
css
.container {
container-type: inline-size; /* 只监控行内方向(通常是宽度)的尺寸 */
container-type: size; /* 监控宽度和高度 */
container-type: normal; /* 默认值,不作为查询容器 */
}为什么通常用 inline-size 而不是 size? 因为 size 需要容器有明确的高度定义(否则会导致循环依赖——高度由内容决定,内容又取决于高度查询结果)。绝大多数场景只需要查询宽度。
container-name(可选)
给容器命名,允许在查询中指定目标容器:
css
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
.main {
container-type: inline-size;
container-name: main;
}
/* 只在 sidebar 容器中应用 */
@container sidebar (max-width: 300px) {
.nav-item { font-size: 14px; }
}如果不指定名称,@container 会查询最近的祖先容器。
容器查询单位
容器查询引入了新的相对单位:
| 单位 | 含义 | 类比 |
|---|---|---|
cqw | 容器宽度的 1% | 类比 vw |
cqh | 容器高度的 1% | 类比 vh |
cqi | 容器行内尺寸的 1% | |
cqb | 容器块尺寸的 1% | |
cqmin | cqi 和 cqb 中较小的 | 类比 vmin |
cqmax | cqi 和 cqb 中较大的 | 类比 vmax |
css
.card-title {
font-size: clamp(1rem, 3cqw, 2rem); /* 根据容器宽度动态调整字号 */
}clamp() 和流体排版
clamp() 函数
clamp(min, preferred, max) 返回一个在最小值和最大值之间的值,优先使用中间的"首选值"。
css
.title {
font-size: clamp(1rem, 2.5vw, 3rem);
/*
屏幕很窄时:font-size = 1rem(最小值兜底)
屏幕适中时:font-size = 2.5vw(随视口缩放)
屏幕很宽时:font-size = 3rem(最大值封顶)
*/
}字号
↑
3rem ─────────────────────────────── max
╱
╱
╱ ← preferred (2.5vw)
╱
1rem ───────────── min
└──────────────────────────────────→ 视口宽度
~400px ~1200px用 clamp() 实现流体排版
传统做法需要多个媒体查询断点来调整字号,clamp() 一行搞定:
css
/* 传统做法 */
h1 { font-size: 1.5rem; }
@media (min-width: 768px) { h1 { font-size: 2rem; } }
@media (min-width: 1200px) { h1 { font-size: 3rem; } }
/* clamp() 做法 */
h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}推荐的首选值写法:使用 rem + vw 组合而非纯 vw,确保用户调整浏览器字号设置时仍然有效。
css
/* ❌ 纯 vw:用户调整字号设置时不会变化 */
font-size: clamp(1rem, 3vw, 2rem);
/* ✅ rem + vw 组合:尊重用户字号偏好 */
font-size: clamp(1rem, 0.5rem + 2vw, 2rem);clamp() 用于间距和宽度
css
.container {
width: clamp(320px, 90vw, 1200px);
padding: clamp(16px, 4vw, 48px);
gap: clamp(8px, 2vw, 24px);
}移动端适配方案
方案一:rem 适配
核心思路:以根元素 font-size 为基准,所有尺寸用 rem 单位表示。动态调整根元素字号即可等比缩放。
javascript
function setRem() {
const designWidth = 375;
const baseFontSize = 100;
const clientWidth = document.documentElement.clientWidth;
const fontSize = (clientWidth / designWidth) * baseFontSize;
document.documentElement.style.fontSize = fontSize + 'px';
}
setRem();
window.addEventListener('resize', setRem);css
/* 设计稿上 32px 的字号 → 32/100 = 0.32rem */
.title {
font-size: 0.32rem;
}
/* 设计稿上 200px 的宽度 → 200/100 = 2rem */
.card {
width: 2rem;
}配合 postcss-pxtorem 自动转换:
javascript
// postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 100,
propList: ['*'],
selectorBlackList: ['no-rem']
}
}
}写代码时直接写 px,构建时自动转换为 rem。
优点:
- 等比缩放,还原度高
- 社区生态成熟(lib-flexible、postcss-pxtorem)
缺点:
- 需要 JavaScript 计算,首屏可能闪烁
rem是全局的,不利于组件隔离- 在大屏设备上也等比放大,可能导致元素过大
方案二:vw 适配
核心思路:直接使用 vw 单位,不依赖 JavaScript。
css
/* 设计稿 375px 宽,一个 200px 的元素:200/375 * 100 = 53.33vw */
.card {
width: 53.33vw;
font-size: 4.27vw; /* 16/375 * 100 */
}配合 postcss-px-to-viewport 自动转换:
javascript
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
unitPrecision: 5,
viewportUnit: 'vw',
selectorBlackList: ['no-vw'],
minPixelValue: 1
}
}
}优点:
- 纯 CSS 方案,不依赖 JavaScript
- 不会有首屏闪烁
- 浏览器原生支持
缺点:
- 大屏等比放大问题(同 rem)
vw相对于视口,组件复用时不够灵活
方案三:clamp() + vw(现代推荐方案)
结合 clamp() 避免等比放大的极端情况:
css
.title {
font-size: clamp(14px, 4.27vw, 22px);
}
.container {
padding: clamp(12px, 4vw, 32px);
}优点:
- 纯 CSS,无 JavaScript 依赖
- 有上下限,避免极端尺寸
- 最符合响应式设计理念
方案四:响应式断点 + Flex/Grid
不做等比缩放,而是在关键断点处改变布局结构:
css
.page {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.page {
grid-template-columns: 240px 1fr;
}
}
@media (min-width: 1200px) {
.page {
grid-template-columns: 240px 1fr 200px;
}
}这是桌面端项目最常用的方式,移动端 H5 项目则更多使用 rem 或 vw。
方案对比
| 方案 | JavaScript | 大屏表现 | 组件复用 | 适用场景 |
|---|---|---|---|---|
| rem | 需要 | 需限制最大值 | 一般 | 移动端 H5 |
| vw | 不需要 | 需限制最大值 | 一般 | 移动端 H5 |
| clamp() + vw | 不需要 | 自带上下限 | 好 | 通用 |
| 断点 + Flex/Grid | 不需要 | 最好 | 最好 | PC 端 / 跨端 |
弹性图片与媒体
基础:图片随容器缩放
css
img {
max-width: 100%;
height: auto;
}这两行代码确保图片不会超出容器宽度,同时保持宽高比。这是响应式设计的基础设置。
响应式图片:srcset 和 sizes
html
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w
"
sizes="
(max-width: 480px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
alt="Responsive image"
/>srcset:告诉浏览器有哪些尺寸的图片可用sizes:告诉浏览器在不同视口宽度下,图片的显示宽度- 浏览器根据设备 DPR 和显示宽度自动选择最优图片
手机(375px,2x DPR):
显示宽度 = 375 × 100% = 375px
需要的像素 = 375 × 2 = 750px
→ 选择 image-800.jpg
桌面(1440px,1x DPR):
显示宽度 = 1440 × 33% = 475px
需要的像素 = 475 × 1 = 475px
→ 选择 image-800.jpg(最接近且大于需求的)<picture> 元素:艺术方向
当不同设备需要展示不同裁剪/构图的图片时:
html
<picture>
<source media="(max-width: 480px)" srcset="hero-mobile.jpg">
<source media="(max-width: 1024px)" srcset="hero-tablet.jpg">
<img src="hero-desktop.jpg" alt="Hero image">
</picture>aspect-ratio 保持宽高比
css
.video-container {
width: 100%;
aspect-ratio: 16 / 9;
}
.avatar {
width: 80px;
aspect-ratio: 1; /* 正方形 */
border-radius: 50%;
}传统 padding hack(在 aspect-ratio 出现之前的方案):
css
.video-container {
position: relative;
width: 100%;
padding-top: 56.25%; /* 9/16 = 0.5625 */
}
.video-container > iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}aspect-ratio 一行代码替代了这个 hack。
响应式设计模式
模式一:列下沉(Column Drop)
多列布局在窄屏时逐个下沉为单列:
桌面: 平板: 手机:
┌────┬────┬────┐ ┌────┬────┐ ┌────────┐
│ A │ B │ C │ │ A │ B │ │ A │
│ │ │ │ │ │ │ ├────────┤
│ │ │ │ ├────┴────┤ │ B │
└────┴────┴────┘ │ C │ ├────────┤
└────────┘ │ C │
└────────┘css
.layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}模式二:侧边栏布局切换
css
.page {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.page {
grid-template-columns: 240px 1fr;
}
}移动端隐藏侧边栏或变为抽屉菜单。
模式三:导航栏适配
桌面:
┌──────────────────────────────────┐
│ Logo [Nav1] [Nav2] [Nav3] [Nav4] │
└──────────────────────────────────┘
移动端:
┌──────────────────┐
│ Logo [☰] │ ← 汉堡菜单
└──────────────────┘css
.nav-links {
display: flex;
gap: 24px;
}
.hamburger {
display: none;
}
@media (max-width: 768px) {
.nav-links {
display: none;
}
.nav-links.open {
display: flex;
flex-direction: column;
position: absolute;
top: 64px;
left: 0;
right: 0;
background: white;
}
.hamburger {
display: block;
}
}模式四:内容优先级
在小屏幕上隐藏次要内容,只展示核心信息:
css
.secondary-info {
display: none;
}
@media (min-width: 768px) {
.secondary-info {
display: block;
}
}
/* 表格场景:隐藏次要列 */
.table-cell.priority-low {
display: none;
}
@media (min-width: 1024px) {
.table-cell.priority-low {
display: table-cell;
}
}经典面试题解析
题目一:rem、em、vw、% 有什么区别?
| 单位 | 相对于 | 说明 |
|---|---|---|
rem | 根元素 <html> 的 font-size | 1rem = 根元素字号(默认 16px) |
em | 当前元素的 font-size | 用于 font-size 时相对于父元素,用于其他属性时相对于自身 font-size |
vw | 视口宽度 | 1vw = 视口宽度的 1% |
% | 父元素的对应属性 | width: 50% 是父元素宽度的 50%;font-size: 150% 是父元素字号的 150% |
em 的两面性是容易出错的点:
css
.box {
font-size: 20px;
padding: 2em; /* 2 × 20 = 40px(相对于自身 font-size) */
}
.child {
font-size: 2em; /* 2 × 父元素 font-size(相对于父元素) */
}题目二:如何实现 1px 边框?
高 DPR 设备上,CSS 的 1px 实际渲染为 2-3 个物理像素,看起来比设计稿粗。
方案一:transform 缩放
css
.border-1px {
position: relative;
}
.border-1px::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background: #ccc;
transform: scaleY(0.5);
transform-origin: bottom;
}方案二:border-width: 0.5px
css
@media (-webkit-min-device-pixel-ratio: 2) {
.border {
border-width: 0.5px;
}
}iOS 8+ 支持,部分 Android 不支持。
方案三:viewport 缩放(整页缩放,现在不推荐)
题目三:移动端如何处理 300ms 点击延迟?
移动端浏览器有 300ms 延迟是为了判断用户是否要双击缩放。
现代解决方案:
html
<meta name="viewport" content="width=device-width, initial-scale=1.0">设置了 width=device-width 后,Chrome 32+ 和其他现代浏览器会自动禁用 300ms 延迟。
CSS 方案:
css
html {
touch-action: manipulation; /* 禁用双击缩放,消除延迟 */
}题目四:媒体查询和容器查询的核心区别是什么?
| 维度 | 媒体查询 | 容器查询 |
|---|---|---|
| 查询对象 | 视口(Viewport) | 父容器 |
| 关注点 | 页面整体布局 | 组件级适配 |
| 组件复用性 | 差(依赖全局视口) | 好(只关心自身容器) |
| 使用场景 | 页面骨架布局 | 可复用组件的响应式 |
| 浏览器支持 | 全部 | 现代浏览器(2023+) |
最佳实践:页面级布局用媒体查询,组件级适配用容器查询。两者结合使用。
题目五:为什么推荐 Mobile First?
- 性能:移动设备优先加载简单样式,桌面端通过
min-width媒体查询添加增强样式。移动设备不需要下载和解析桌面端的复杂样式 - 渐进增强:从基础体验开始,逐步添加功能,确保低端设备也能正常使用
- 心智模型:从简单到复杂比从复杂到简单更容易设计和维护
- 用户占比:全球移动端流量超过 50%,应优先保证移动体验
追问思考
100vh在 iOS Safari 中的表现和桌面浏览器有什么不同?dvh/svh/lvh分别解决了什么问题?- 容器查询的
container-type: size为什么可能导致循环依赖?浏览器如何处理? - rem 方案和 vw 方案在"用户调整浏览器默认字号"这个场景下表现有何不同?哪个更符合无障碍标准?
prefers-reduced-motion媒体特性在实际项目中应该如何使用?不处理会有什么后果?- 如果要在一个已有项目中从 Desktop First 迁移到 Mobile First,应该如何规划迁移步骤?