响应式设计

What — 是什么

响应式设计(Responsive Design)让网页根据不同屏幕尺寸自动调整布局和样式,实现一套代码适配多端。

核心原则

  1. 流体布局(Fluid Grid):使用相对单位(%、vw、rem)代替固定 px,布局随视口自动缩放
  2. 弹性图片(Flexible Images):图片随容器缩放,不超出父元素宽度
  3. 媒体查询(Media Queries):根据视口特性应用不同样式
  4. 容器查询(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%全屏高度
vminmin(vw, vh)正方形/居中
vmaxmax(vw, vh)全屏背景
dvh动态视口高度移动端全屏(排除地址栏)
svh小视口高度移动端最小安全高度
lvh大视口高度移动端最大可用高度

断点策略

移动优先 vs 桌面优先:

维度移动优先(推荐)桌面优先
基础样式小屏大屏
媒体查询min-width 递增max-width 递减
CSS 体积小屏先加载(渐进增强)大屏先加载(优雅降级)
移动性能优(基础样式即移动端)差(需覆盖桌面样式)
语义从简到繁,符合认知从繁到简,容易遗漏

常用断点参考:

断点名称宽度范围典型设备
xs0 - 575px小手机
sm576 - 767px大手机
md768 - 991px平板竖屏
lg992 - 1199px平板横屏/小桌面
xl1200 - 1399px标准桌面
2xl1400px+大屏桌面

媒体查询完整语法

/* 基础用法 */
@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 媒体查询
字体过小移动端用了固定 pxrem + 根字体缩放
图片模糊高 DPR 设备加载 1x 图srcset 提供 2x/3x 图
iOS 弹性滚动-webkit-overflow-scrolling: touchoverscroll-behavior: contain
安卓键盘弹起视口高度变化导致布局错乱visualViewport API 监听
固定定位在移动端异常iOS Safari 的 viewport bug避免嵌套 fixed,用 position: sticky

最佳实践

  • 移动优先,min-width 递增
  • 断点不用太多:768px(平板)、1024px(桌面)、1280px(大屏)
  • 间距用 clamp() 流式缩放,减少媒体查询
  • 组件级用容器查询,页面级用媒体查询
  • 响应式图片用 srcset + sizes + picture
  • 用 CSS 自定义变量管理断点相关值
  • 尊重 prefers-reduced-motionprefers-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 显式声明容器。容器查询更适合组件库开发,媒体查询更适合页面级布局。


相关链接: