响应式设计
问题
什么是响应式设计?媒体查询怎么用?rem、em、vw/vh 等单位有什么区别?Container Queries 解决了什么问题?
答案
响应式设计概述
响应式设计(Responsive Web Design, RWD)的核心思想是让一套代码自适应不同屏幕尺寸。三大基础技术:
- 流式布局:使用百分比、Flex、Grid 等弹性布局
- 媒体查询:根据设备特征应用不同样式
- 弹性媒体:图片/视频随容器自适应
<!-- viewport meta 标签(响应式的前提条件) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
媒体查询(Media Queries)
基本语法
/* 屏幕宽度 ≤ 768px 时生效 */
@media screen and (max-width: 768px) {
.sidebar { display: none; }
}
/* 屏幕宽度 ≥ 1024px 时生效 */
@media screen and (min-width: 1024px) {
.container { max-width: 1200px; }
}
现代语法(Range Syntax)
/* 新语法(Chrome 104+、Firefox 63+、Safari 16.4+) */
@media (width <= 768px) { }
@media (768px <= width <= 1024px) { }
@media (width >= 1024px) { }
常用媒体特征
| 特征 | 说明 | 示例 |
|---|---|---|
width / height | 视口宽高 | (min-width: 768px) |
orientation | 方向 | (orientation: portrait) |
prefers-color-scheme | 用户色彩偏好 | (prefers-color-scheme: dark) |
prefers-reduced-motion | 减少动画 | (prefers-reduced-motion: reduce) |
hover | 是否支持悬停 | (hover: hover) |
pointer | 指针精度 | (pointer: coarse) 触屏 |
resolution | 屏幕分辨率 | (min-resolution: 2dppx) |
display-mode | 显示模式 | (display-mode: standalone) PWA |
逻辑运算
@media screen and (min-width: 768px) and (max-width: 1024px) { } /* AND */
@media (max-width: 768px), (orientation: portrait) { } /* OR */
@media not print { } /* NOT */
常用断点
/* Mobile first(推荐) */
/* 默认样式 → 手机 */
.container { padding: 16px; }
/* 平板 */
@media (min-width: 768px) {
.container { padding: 24px; }
}
/* 桌面 */
@media (min-width: 1024px) {
.container { max-width: 1200px; margin: 0 auto; }
}
/* 大屏 */
@media (min-width: 1440px) {
.container { max-width: 1400px; }
}
- Mobile First(
min-width,从小到大):推荐,移动端优先,渐进增强 - Desktop First(
max-width,从大到小):桌面端优先,优雅降级
Mobile First 性能更好——移动设备只需解析默认样式,无需匹配媒体查询。
CSS 单位
相对单位对比
| 单位 | 相对于 | 说明 |
|---|---|---|
em | 父元素的 font-size | 可累积,嵌套时会逐层放大/缩小 |
rem | 根元素(<html>)的 font-size | 不受嵌套影响,推荐用于全局 |
% | 父元素的对应属性 | width: 50% = 父宽度的一半 |
vw / vh | 视口宽度/高度的 1% | 100vw = 视口宽度 |
vmin / vmax | 视口较小/较大边的 1% | 常用于保持比例 |
svw / svh | 小视口(不含地址栏) | 移动端地址栏收起时 |
lvw / lvh | 大视口(含地址栏) | 移动端地址栏展开时 |
dvw / dvh | 动态视口 | 推荐,自动适应地址栏变化 |
ch | 字符 0 的宽度 | 适合限制每行字符数 |
lh | 当前行高 | |
cqw / cqh | 容器查询宽/高的 1% | 配合 Container Queries |
在移动端浏览器中,100vh 包含了地址栏区域,导致内容被遮挡。使用 100dvh(动态视口高度)或以下方案解决:
.full-height {
/* 兼容方案 */
height: 100vh;
height: 100dvh; /* 覆盖,支持的浏览器会使用这个 */
}
em vs rem
html { font-size: 16px; }
.parent {
font-size: 20px;
}
.child {
font-size: 1.5em; /* 20px × 1.5 = 30px(相对父元素) */
font-size: 1.5rem; /* 16px × 1.5 = 24px(相对根元素) */
padding: 1em; /* 当在 font-size 以外使用时,em 相对于当前元素的 font-size */
}
rem 适配方案
/* 基础 rem 方案 */
html {
font-size: 16px;
}
@media (min-width: 768px) {
html { font-size: 18px; }
}
/* 或使用 clamp() 实现平滑过渡 */
html {
/* 最小 14px,理想 1vw + 10px,最大 18px */
font-size: clamp(14px, 1vw + 10px, 18px);
}
Container Queries(容器查询)
Container Queries 根据父容器的尺寸(而非视口)来应用样式,解决了组件在不同容器中自适应的问题。
/* 1. 声明容器 */
.card-wrapper {
container-type: inline-size; /* 声明为容器 */
container-name: card; /* 命名(可选) */
}
/* 2. 基于容器宽度应用样式 */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}
.wrapper {
container-type: inline-size; /* 在行内轴(通常是宽度)上建立容器 */
container-type: size; /* 宽高都建立容器 */
container-type: normal; /* 不建立容器(默认) */
}
- 媒体查询:基于视口尺寸 → 适合页面级布局
- 容器查询:基于父容器尺寸 → 适合组件级自适应
同一个卡片组件,在侧边栏中可能是竖版,在主内容区可能是横版——这正是 Container Queries 解决的问题。
响应式图片
<!-- srcset + sizes -->
<img
src="small.jpg"
srcset="small.jpg 480w, medium.jpg 768w, large.jpg 1200w"
sizes="(max-width: 768px) 100vw, 50vw"
alt="响应式图片"
/>
<!-- picture 元素 -->
<picture>
<source media="(min-width: 1024px)" srcset="desktop.webp" type="image/webp" />
<source media="(min-width: 768px)" srcset="tablet.webp" type="image/webp" />
<img src="mobile.jpg" alt="响应式图片" />
</picture>
clamp() 流式排版
clamp(min, preferred, max) 实现无断点的平滑缩放:
h1 {
/* 最小 24px,理想值 5vw,最大 48px */
font-size: clamp(1.5rem, 5vw, 3rem);
}
.container {
/* 最小 320px,理想值 90vw,最大 1200px */
width: clamp(320px, 90vw, 1200px);
margin: 0 auto;
}
.gap {
/* 间距也可以响应式 */
gap: clamp(16px, 3vw, 48px);
}
常见面试问题
Q1: rem 和 em 有什么区别?
答案:
| 特性 | em | rem |
|---|---|---|
| 相对于 | 父元素(或自身用于非font-size属性时) | 根元素 <html> |
| 嵌套累积 | 会(层层乘以父级) | 不会 |
| 使用场景 | 组件内部间距(与字号成比例) | 全局字号、间距 |
最佳实践:font-size 用 rem,组件内 padding/margin 用 em。
Q2: 移动端 1px 边框问题怎么解决?
答案:
高 DPR 设备上 1px CSS 像素可能显示为 2-3 物理像素,看起来过粗。解决方案:
/* 方案1: transform 缩放(推荐) */
.border-1px::after {
content: '';
position: absolute;
left: 0; top: 0;
width: 200%; height: 200%;
border: 1px solid #ccc;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
}
/* 方案2: 0.5px(仅 iOS Safari 支持) */
.border { border-width: 0.5px; }
/* 方案3: box-shadow */
.border { box-shadow: 0 0 0 0.5px #ccc; }
Q3: 什么是 Mobile First?为什么推荐?
答案:
Mobile First 即默认样式面向手机,通过 min-width 媒体查询逐渐添加大屏样式:
/* 默认:手机样式 */
.nav { flex-direction: column; }
/* 平板及以上 */
@media (min-width: 768px) {
.nav { flex-direction: row; }
}
推荐原因:
- 性能:移动设备不需要匹配额外的媒体查询
- 渐进增强:基础功能保证,大屏逐步增强
- 符合用户趋势:移动端流量占比超过 60%
Q4: vw/vh 和百分比有什么区别?
答案:
| 特性 | vw / vh | % |
|---|---|---|
| 相对于 | 视口(viewport) | 父元素 |
width: 50vw | 视口宽度的一半 | — |
width: 50% | — | 父元素宽度的一半 |
| 受父元素影响 | 否 | 是 |
常见问题:100vw 在有滚动条的页面中会包含滚动条宽度,导致横向溢出。解决方案:
.full-width {
width: 100%; /* 不包含滚动条 */
/* 或 */
width: 100vw;
margin-left: calc(-50vw + 50%); /* 居中修正 */
}
Q5: Container Queries 和 Media Queries 的区别?
答案:
| 特性 | Media Queries | Container Queries |
|---|---|---|
| 查询对象 | 视口 | 父容器 |
| 适用场景 | 页面布局 | 组件自适应 |
| 组件复用 | 同一组件在不同位置表现一致 | 同一组件根据容器不同而不同 |
| 兼容性 | 所有浏览器 | Chrome 105+、Safari 16+、Firefox 110+ |
| 语法 | @media | @container |
Container Queries 最大价值:让组件真正独立,不依赖页面布局。
Q6: clamp() 函数怎么用?
答案:
clamp(min, preferred, max) 返回介于 min 和 max 之间的 preferred 值:
/* 流式字号:最小16px,理想4vw,最大32px */
font-size: clamp(1rem, 4vw, 2rem);
/* 等价于 */
font-size: max(1rem, min(4vw, 2rem));
适用场景:字号、间距、容器宽度的平滑响应式缩放,避免硬性断点。
Q7: 如何处理移动端 100vh 的问题?
答案:
移动端浏览器地址栏会动态显示/隐藏,导致 100vh 高度不准确:
/* 方案1: dvh(推荐) */
.hero { height: 100dvh; }
/* 方案2: svh(地址栏展开时的高度) */
.hero { height: 100svh; }
/* 方案3: JS 填充 */
.hero { height: calc(var(--vh, 1vh) * 100); }
function setVH(): void {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
window.addEventListener('resize', setVH);
setVH();
Q8: 什么是 prefers-color-scheme?如何实现暗色模式?
答案:
/* 检测系统暗色模式偏好 */
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #e0e0e0;
}
}
@media (prefers-color-scheme: light) {
:root {
--bg: #ffffff;
--text: #333333;
}
}
完整暗色模式方案通常结合 CSS 变量 + JS 切换,详见 CSS 变量与主题切换。