CSS 响应式设计
响应式设计使网页能够适配不同尺寸的设备,从手机到桌面显示器。随着 CSS 的发展,现代响应式设计已经从单纯的媒体查询扩展到容器查询、数学函数等更强大的工具。
响应式设计基础
什么是响应式设计?
响应式设计是一种让网页布局根据设备屏幕尺寸自动调整的方法:
- 移动优先:从小屏幕开始设计
- 流式布局:使用相对单位而非固定像素
- 媒体查询:根据屏幕条件应用不同样式
- 容器查询:根据容器尺寸调整样式(现代方法)
viewport 视口
<!-- 必须设置! -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
视口属性说明:
| 属性 | 说明 |
|---|---|
| width | 视口宽度,device-width 表示设备宽度 |
| initial-scale | 初始缩放比例 |
| minimum-scale | 最小缩放比例 |
| maximum-scale | 最大缩放比例 |
| user-scalable | 是否允许用户缩放 |
媒体查询 @media
媒体查询是响应式设计的基础,它允许我们根据视口大小、设备特性等条件应用不同的样式。
基本语法
/* 屏幕宽度小于768px时生效 */
@media (max-width: 768px) {
body {
font-size: 14px;
}
}
媒体类型
/* screen: 屏幕设备 */
@media screen { }
/* print: 打印机 */
@media print {
.navbar, .sidebar { display: none; }
}
/* all: 所有设备(默认) */
@media all { }
媒体条件
/* 最小宽度 */
@media (min-width: 768px) { }
/* 最大宽度 */
@media (max-width: 768px) { }
/* 宽度范围 */
@media (min-width: 768px) and (max-width: 1024px) { }
/* 屏幕方向 */
@media (orientation: portrait) { } /* 竖屏 */
@media (orientation: landscape) { } /* 横屏 */
/* 设备像素比(高清屏) */
@media (-webkit-min-device-pixel-ratio: 2) { }
@media (resolution: 2dppx) { }
/* 悬停能力 */
@media (hover: hover) { } /* 支持悬停的设备 */
@media (hover: none) { } /* 不支持悬停的设备(触摸屏) */
/* 指针精度 */
@media (pointer: fine) { } /* 精确指针(鼠标) */
@media (pointer: coarse) { } /* 粗略指针(触摸) */
/* 用户偏好 */
@media (prefers-color-scheme: dark) { } /* 深色模式 */
@media (prefers-color-scheme: light) { } /* 浅色模式 */
@media (prefers-reduced-motion: reduce) { } /* 减少动画 */
常用断点
断点的选择应该基于内容需要变化的时机,而不是特定设备。以下是常用的参考断点:
/* 移动端(小屏手机) */
@media (max-width: 575px) { }
/* 平板竖屏(大屏手机) */
@media (min-width: 576px) and (max-width: 767px) { }
/* 平板横屏 */
@media (min-width: 768px) and (max-width: 991px) { }
/* 桌面端 */
@media (min-width: 992px) and (max-width: 1199px) { }
/* 大桌面 */
@media (min-width: 1200px) { }
- 优先使用
min-width(移动优先) - 断点应该根据内容调整,而非设备
- 避免过多断点,保持简洁
容器查询 @container
容器查询是 CSS 的现代特性,它允许元素根据其容器的大小而非视口大小来调整样式。这使得组件更加独立和可复用——无论组件被放在页面的哪个位置,都能自动适应其容器。
容器查询与媒体查询的区别
┌─────────────────────────────────────────────────────────────────┐
│ 媒体查询 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 视口(Viewport) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 组件 A │ │ 组件 B │ │ 组件 C │ │ │
│ │ │ 相同样式 │ │ 相同样式 │ │ 相同样式 │ ← 同时响应 │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 容器查询 │
│ ┌──────────────┐ ┌────────────────────┐ │
│ │ 容器 1 │ │ 容器 2 │ │
│ │ ┌─────────┐ │ 宽度 300px │ ┌──────────────┐ │ │
│ │ │ 组件 A │ │ → 紧凑布局 │ │ 组件 B │ │ │
│ │ │ 紧凑布局 │ │ │ │ 宽松布局 │ │ │
│ │ └─────────┘ │ │ └──────────────┘ │ │
│ └──────────────┘ └────────────────────┘ │
│ 宽度 500px → 宽松布局 │
└─────────────────────────────────────────────────────────────────┘
创建容器上下文
要使用容器查询,首先需要声明一个容器上下文:
/* 使用 container-type 属性 */
.sidebar {
container-type: inline-size;
}
container-type 的值:
| 值 | 说明 |
|---|---|
size | 基于容器的行内和块尺寸查询 |
inline-size | 基于容器的行内尺寸查询(最常用) |
normal | 默认值,不作为尺寸查询容器 |
大多数情况下使用 inline-size 即可,它基于容器的宽度(在水平书写模式下)进行查询,性能开销更小。
基本用法
<div class="sidebar">
<article class="card">
<img src="photo.jpg" alt="">
<div class="card-content">
<h3>标题</h3>
<p>内容描述...</p>
</div>
</article>
</div>
/* 声明容器 */
.sidebar {
container-type: inline-size;
}
/* 默认样式:紧凑布局 */
.card {
display: flex;
flex-direction: column;
padding: 1rem;
}
.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
/* 容器宽度大于 400px 时切换布局 */
@container (min-width: 400px) {
.card {
flex-direction: row;
gap: 1rem;
}
.card img {
width: 200px;
aspect-ratio: 1;
}
}
命名容器
当页面有多个容器时,可以为容器命名以便精确查询:
/* 使用 container 简写属性 */
.sidebar {
container: sidebar / inline-size;
}
.main-content {
container: main / inline-size;
}
/* 查询特定容器 */
@container sidebar (min-width: 300px) {
.widget { /* 样式 */ }
}
@container main (min-width: 600px) {
.article { /* 样式 */ }
}
也可以分别使用 container-name 和 container-type:
.sidebar {
container-name: sidebar;
container-type: inline-size;
}
容器查询单位
容器查询提供了专门的长度单位,相对于容器尺寸计算:
| 单位 | 说明 |
|---|---|
cqw | 容器宽度的 1% |
cqh | 容器高度的 1% |
cqi | 容器行内尺寸的 1%(常用) |
cqb | 容器块尺寸的 1% |
cqmin | cqi 和 cqb 中较小的值 |
cqmax | cqi 和 cqb 中较大的值 |
.card-title {
/* 字体大小根据容器宽度变化 */
font-size: clamp(1rem, 5cqi, 2rem);
}
.card {
/* 内边距相对于容器 */
padding: 2cqi;
}
如果没有可用的查询容器,容器查询单位会回退到小视口单位(svw、svh 等)。
容器查询支持的特性
容器查询支持多种尺寸特性:
/* 宽度查询 */
@container (min-width: 400px) { }
/* 高度查询 */
@container (min-height: 300px) { }
/* 行内尺寸 */
@container (inline-size > 500px) { }
/* 块尺寸 */
@container (block-size > 400px) { }
/* 宽高比 */
@container (aspect-ratio > 16/9) { }
/* 方向 */
@container (orientation: landscape) { }
/* 组合条件 */
@container (min-width: 400px) and (orientation: landscape) { }
完整示例:响应式卡片组件
<!-- 卡片可以在不同容器中自适应 -->
<div class="layout">
<aside class="sidebar">
<article class="card">
<img src="photo.jpg" alt="">
<div class="card-body">
<h3>文章标题</h3>
<p>文章简介...</p>
</div>
</article>
</aside>
<main class="content">
<article class="card">
<img src="photo.jpg" alt="">
<div class="card-body">
<h3>文章标题</h3>
<p>文章简介...</p>
</div>
</article>
</main>
</div>
/* 声明容器 */
.sidebar {
container: sidebar / inline-size;
width: 280px;
}
.content {
container: main / inline-size;
flex: 1;
}
/* 卡片基础样式 */
.card {
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s;
}
.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.card-body {
padding: 1rem;
}
/* 窄容器:垂直布局 */
@container (max-width: 399px) {
.card {
font-size: 0.9rem;
}
.card-body {
padding: 0.75rem;
}
}
/* 中等容器:水平布局 */
@container (min-width: 400px) {
.card {
flex-direction: row;
}
.card img {
width: 180px;
aspect-ratio: 1;
}
.card-body {
display: flex;
flex-direction: column;
justify-content: center;
}
}
/* 宽容器:更大图片 */
@container main (min-width: 600px) {
.card img {
width: 250px;
}
.card-body {
padding: 1.5rem;
}
}
嵌套容器查询
容器查询可以嵌套,实现更精细的控制:
@container (min-width: 800px) {
.parent {
/* 外层容器样式 */
@container (min-width: 400px) {
.child {
/* 内层容器样式 */
}
}
}
}
容器查询 vs 媒体查询选择指南
| 场景 | 推荐使用 |
|---|---|
| 整体页面布局 | 媒体查询 |
| 固定位置的元素(导航栏、页脚) | 媒体查询 |
| 可复用组件 | 容器查询 |
| 组件放在不同容器中 | 容器查询 |
| 需要响应视口变化 | 媒体查询 |
| 需要响应容器变化 | 容器查询 |
/* 页面级别:媒体查询 */
@media (min-width: 768px) {
.page-layout {
display: grid;
grid-template-columns: 250px 1fr;
}
}
/* 组件级别:容器查询 */
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
现代响应式函数
CSS 提供了 clamp()、min() 和 max() 函数,可以在不使用媒体查询的情况下实现响应式效果。
clamp() 函数
clamp() 函数在最小值和最大值之间选择一个中间值,非常适合响应式字体、间距等。
/* 语法:clamp(最小值, 首选值, 最大值) */
.element {
/* 字体大小在 1rem 到 2rem 之间,优先使用 2.5vw */
font-size: clamp(1rem, 2.5vw, 2rem);
}
工作原理:
- 如果首选值小于最小值,使用最小值
- 如果首选值大于最大值,使用最大值
- 否则使用首选值
常用场景:
/* 响应式标题 */
h1 {
font-size: clamp(1.5rem, 5vw, 3rem);
}
/* 响应式容器宽度 */
.container {
width: clamp(300px, 80%, 1200px);
margin: 0 auto;
}
/* 响应式间距 */
.section {
padding: clamp(1rem, 5vw, 3rem);
}
/* 响应式行高 */
p {
line-height: clamp(1.4, 1.5 + 0.5vw, 1.8);
}
min() 函数
min() 函数返回参数列表中的最小值:
/* 取 50% 和 400px 中较小的那个 */
.sidebar {
width: min(50%, 400px);
/* 当容器小于 800px 时,宽度为 50% */
/* 当容器大于 800px 时,宽度固定为 400px */
}
/* 响应式字体上限 */
.title {
font-size: min(5vw, 48px);
}
/* 响应式间距上限 */
.content {
padding: min(10vw, 80px);
}
max() 函数
max() 函数返回参数列表中的最大值:
/* 取 300px 和 50% 中较大的那个 */
.main {
width: max(300px, 50%);
/* 当容器小于 600px 时,宽度固定为 300px */
/* 当容器大于 600px 时,宽度为 50% */
}
/* 最小字体保证可读性 */
.text {
font-size: max(16px, 1.2vw);
}
/* 最小间距 */
.section {
padding: max(1rem, 5vw);
}
函数对比
| 函数 | 作用 | 典型场景 |
|---|---|---|
min() | 选择最小值 | 设置最大边界 |
max() | 选择最大值 | 设置最小边界 |
clamp() | 限制范围 | 同时设置最小、首选、最大值 |
/* 三者等价关系 */
/* clamp(min, val, max) 等价于 max(min, min(val, max)) */
/* 使用 clamp(推荐,更直观) */
font-size: clamp(1rem, 2.5vw, 2rem);
/* 使用 min/max 组合 */
font-size: max(1rem, min(2.5vw, 2rem));
响应式布局策略
1. 移动优先 vs 桌面优先
移动优先(推荐)
从小屏幕开始设计,逐步添加大屏幕样式:
/* 默认:移动端样式 */
.container {
width: 100%;
padding: 0 15px;
}
/* 平板及以上 */
@media (min-width: 768px) {
.container {
max-width: 750px;
margin: 0 auto;
}
}
/* 桌面及以上 */
@media (min-width: 992px) {
.container { max-width: 970px; }
}
/* 大桌面 */
@media (min-width: 1200px) {
.container { max-width: 1170px; }
}
桌面优先
从大屏幕开始设计,逐步添加小屏幕样式:
/* 默认:桌面样式 */
.container {
max-width: 1170px;
margin: 0 auto;
}
/* 平板及以下 */
@media (max-width: 991px) {
.container { max-width: 750px; }
}
/* 手机及以下 */
@media (max-width: 767px) {
.container { width: 100%; }
}
- 移动端样式通常更简单,作为基础更合理
- 添加样式比覆盖样式更高效
- 强制考虑性能和内容优先级
- CSS 文件更小
2. 流式网格
使用相对单位和 CSS Grid 创建自适应网格:
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 15px;
}
/* 自动填充网格 */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
/* 或使用 auto-fit(更紧凑) */
.grid-compact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
auto-fill vs auto-fit:
auto-fill:保留空列,网格项不会拉伸auto-fit:空列会塌陷,网格项会拉伸填满
3. Flexbox 响应式
.nav {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.nav-links {
display: flex;
gap: 20px;
}
/* 移动端:垂直排列 */
@media (max-width: 767px) {
.nav {
flex-direction: column;
align-items: stretch;
}
.nav-links {
flex-direction: column;
gap: 10px;
}
}
/* 使用 gap 替代 margin */
.grid {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.grid-item {
flex: 1 1 calc(50% - 10px); /* 两列布局 */
}
@media (min-width: 768px) {
.grid-item {
flex: 1 1 calc(33.333% - 14px); /* 三列布局 */
}
}
4. 图片响应式
/* 基础:图片自适应 */
img {
max-width: 100%;
height: auto;
display: block;
}
/* 使用 srcset 提供多分辨率 */
<img src="small.jpg"
srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="描述">
/* 使用 picture 元素切换图片 */
<picture>
<source media="(max-width: 600px)" srcset="mobile.jpg">
<source media="(min-width: 601px)" srcset="desktop.jpg">
<img src="default.jpg" alt="描述">
</picture>
/* 使用 picture 切换格式 */
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img src="image.jpg" alt="描述">
</picture>
/* 使用 aspect-ratio 保持比例 */
.card-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
5. 条件显示
/* 仅桌面显示 */
.desktop-only {
display: none;
}
/* 仅移动端显示 */
.mobile-only {
display: block;
}
@media (min-width: 768px) {
.desktop-only { display: block; }
.mobile-only { display: none; }
}
/* 使用视觉隐藏(保留可访问性) */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
响应式组件
1. 响应式导航栏
/* 默认:移动端(汉堡菜单) */
.navbar {
display: flex;
flex-direction: column;
padding: 10px;
}
.menu-toggle {
display: block;
align-self: flex-end;
}
.nav-links {
display: none;
flex-direction: column;
}
/* 激活菜单 */
.nav-links.active {
display: flex;
}
/* 平板及以上 */
@media (min-width: 768px) {
.navbar {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.menu-toggle {
display: none;
}
.nav-links {
display: flex;
flex-direction: row;
gap: 20px;
}
}
2. 响应式卡片
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.card {
display: flex;
flex-direction: column;
padding: 20px;
}
.card img {
width: 100%;
height: 200px;
object-fit: cover;
}
@media (max-width: 575px) {
.card {
padding: 15px;
}
.card img {
height: 150px;
}
}
3. 响应式表单
.form-row {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.form-group {
flex: 1;
}
@media (max-width: 575px) {
.form-row {
flex-direction: column;
gap: 0;
}
}
4. 响应式表格
/* 横向滚动 */
.table-container {
overflow-x: auto;
}
/* 卡片式转换 */
@media (max-width: 767px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead { display: none; }
tr {
margin-bottom: 15px;
border: 1px solid #ddd;
}
td {
padding: 10px 10px 10px 40%;
position: relative;
}
td::before {
content: attr(data-label);
position: absolute;
left: 10px;
font-weight: bold;
}
}
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>城市</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="姓名">张三</td>
<td data-label="年龄">25</td>
<td data-label="城市">北京</td>
</tr>
</tbody>
</table>
响应式字体
1. 使用相对单位
html { font-size: 16px; }
body { font-size: 1rem; } /* 16px */
h1 { font-size: 2rem; } /* 32px */
p { font-size: 1rem; } /* 16px */
2. CSS clamp()
/* 响应式字体大小:最小14px,最大24px,优先18px */
h1 {
font-size: clamp(1.75rem, 2.5vw, 3rem);
}
p {
font-size: clamp(0.875rem, 1.5vw, 1.125rem);
}
3. 媒体查询调整
html { font-size: 14px; }
@media (min-width: 768px) {
html { font-size: 16px; }
}
@media (min-width: 1200px) {
html { font-size: 18px; }
}
响应式间距
/* 基础间距 */
.section {
padding: 20px;
}
/* 响应式间距 */
@media (min-width: 768px) {
.section { padding: 40px; }
}
@media (min-width: 1200px) {
.section { padding: 60px 0; }
}
/* 使用 CSS 变量 */
:root {
--spacing-sm: 10px;
--spacing-md: 20px;
--spacing-lg: 40px;
}
@media (min-width: 768px) {
:root {
--spacing-sm: 15px;
--spacing-md: 30px;
--spacing-lg: 60px;
}
}
常用响应式模式
1. 侧边栏布局
.layout {
display: grid;
grid-template-columns: 250px 1fr;
}
@media (max-width: 767px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar { display: none; }
}
2. 卡片网格
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
@media (max-width: 991px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 575px) {
.grid { grid-template-columns: 1fr; }
}
3. 堆叠到并排
.content {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.content {
flex-direction: row;
}
.main { flex: 2; }
.sidebar { flex: 1; }
}
测试工具
浏览器开发者工具
- 按 F12 打开开发者工具
- 点击设备模拟图标(手机图标)
- 选择不同设备或自定义尺寸
在线工具
小结
本章学习了:
- 响应式基础:viewport 设置、移动优先理念
- 媒体查询:条件语法、常用断点、用户偏好检测
- 容器查询:创建容器上下文、命名容器、容器查询单位
- 现代响应式函数:clamp()、min()、max() 实现无媒体查询的响应式
- 布局策略:流式网格、Flexbox 响应式、图片响应式
- 响应式组件:导航、卡片、表单、表格
- 响应式内容:图片、字体、间距
练习
- 使用容器查询创建一个可在不同容器中自适应的卡片组件
- 使用 clamp() 实现响应式字体大小(最小 1rem,最大 2rem)
- 创建一套响应式网格系统,使用 Grid 的 auto-fill/auto-fit
- 实现一个响应式导航栏,包含移动端汉堡菜单
- 将表格转换为移动端卡片布局
- 对比媒体查询和容器查询的使用场景,各创建一个示例
- 使用 min() 和 max() 函数实现侧边栏的响应式宽度