响应式网页设计(responsive web design) 让同一份内容根据可用空间、用户偏好和输入能力改变呈现方式。
只给几个设备宽度添加媒体查询,会漏掉组件容器、缩放、长文本和非鼠标输入造成的约束。
先写可收缩、可换行的基础布局,再用内容触发的媒体查询或容器查询增强,并在边界两侧测试真实内容。
是什么,为什么存在
响应式设计不是为手机、平板和桌面各写一套页面,而是让一个文档在约束变化时仍然可读、可操作。约束不只有视口宽度,还包括容器宽度、文字缩放、屏幕方向、指针能力以及用户对动态效果的偏好。
它解决的是未知显示环境问题。开发时无法穷举窗口大小、分屏比例、嵌入位置、翻译长度和系统设置;如果布局依赖几个假定设备尺寸,中间状态就容易溢出或遮挡内容。响应式规则把这些假设改写成浏览器可以求值的条件。
常见实现由流式尺寸、能够换行或重排的布局、弹性媒体和条件规则组成。CSS Grid、Flexbox、min()、max() 与 clamp() 可以在没有断点时连续适应; 媒体查询(media query) 处理视口和用户环境; 容器查询(container query) 处理可复用组件所在空间。
响应式也不等于把所有内容缩小。导航可能从横排变为展开按钮,数据表可能需要改变信息组织方式,图片可以选择不同候选资源,但文档顺序、名称和任务路径仍应保持一致。
工作原理
浏览器先根据正常层叠计算基础样式,再判断条件规则是否匹配。条件变化后,例如窗口变窄或组件进入较窄栏位,浏览器重新计算受影响元素的样式和布局;JavaScript 通常不需要复制这套判断。
基础规则应该覆盖最受限的可用空间,并保留自然流。随后使用 min-width 或范围语法增加布局能力,这是一种 渐进增强(progressive enhancement) :条件不匹配或某项增强不可用时,核心内容仍然存在。
流式布局先减少断点数量。max-width 限制舒适行长,minmax() 为网格轨道设置可用下限,flex-wrap 允许项目换行,clamp() 则为字号或间距设置下限、流式首选值和上限。固定像素仍可表达边框或经过验证的最小尺寸,但不应成为整页宽度的假定。
.page {
width: min(100% - 2rem, 70rem);
margin-inline: auto;
}
.cards {
display: grid;
gap: clamp(0.75rem, 2vw, 1.5rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}
.cards > * {
min-width: 0;
}
.hero-title {
font-size: clamp(2rem, 1.4rem + 3vw, 4rem);
}
img,
video {
display: block;
max-width: 100%;
height: auto;
}这组基础规则没有猜测设备类型。页面宽度有上限和边距,卡片按自身下限自动成列,标题在明确上下限之间连续缩放,媒体不会超过内容框。
断点(breakpoint) 是布局约束需要离散变化的位置,不是某款设备的代号。把视口缓慢拉宽,在内容开始拥挤、行长失控或控件相撞的位置设断点;再用 em 或 rem 表达与文字尺度相关的条件,避免根字号变化后布局仍锁在原像素假设上。
媒体查询观察视口或用户环境。尺寸查询可以改页面级结构,prefers-reduced-motion 可以选择 减少动态效果(reduced motion) 方案,hover 与 pointer 描述输入能力。后两者不能可靠识别“手机”或“桌面”,混合输入设备也可能同时具有多种能力。
容器查询观察祖先查询容器,而不是视口。设置 container-type: inline-size 后,后代可以通过 @container (width >= 30rem) 按容器行内尺寸切换布局。同一张卡片因此能在主栏横排、在侧栏竖排,无需知道页面模板。
.card-host {
container: card / inline-size;
}
@media (width >= 64rem) {
.page-shell {
grid-template-columns: minmax(0, 1fr) 18rem;
}
}
@container card (width >= 30rem) {
.card {
grid-template-columns: 10rem minmax(0, 1fr);
}
}
@media (hover: hover) and (pointer: fine) {
.menu-button:hover {
text-decoration: underline;
}
}
@media (prefers-reduced-motion: reduce) {
.panel {
scroll-behavior: auto;
}
}这些条件分别读取视口、查询容器、主输入能力和用户偏好。它们回答的是不同问题,不应该折叠成一个“移动端”布尔值。
响应式图片处理的是资源选择和艺术指导。使用宽度描述符的 srcset 时,sizes 告诉浏览器图片在各条件下预计占用的 CSS 宽度,浏览器再结合设备像素比选择候选资源。picture 则允许不同条件使用不同裁切或格式;它不能代替准确的 alt 文本和明确的固有尺寸。
示例
无断点的内在网格
先让内容和可用空间共同决定列数。min() 防止最小轨道在容器本身比 14rem 更窄时造成溢出,长标题也可以在卡片内部断行。
<section class="gallery" aria-label="Plans">
<article class="card"><h2>Starter</h2><p>For personal projects.</p></article>
<article class="card"><h2>Collaboration</h2><p>For growing teams.</p></article>
<article class="card"><h2>EnterpriseSecurityControls</h2><p>For regulated work.</p></article>
</section>
<style>
.gallery {
display: grid;
width: 680px;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
.card {
min-width: 0;
padding: 16px;
border: 1px solid #888;
}
.card h2 {
overflow-wrap: anywhere;
}
</style>
<script>
const gallery = document.querySelector('.gallery');
const tracks = getComputedStyle(gallery).gridTemplateColumns.split(' ').length;
console.log(`${gallery.clientWidth}px: ${tracks} columns`);
</script>680px: 2 columns这个输出来自宽度为 680px 的真实 Chromium 布局。三条 14rem 轨道加两段间距放不下,因此 Grid 建立两列并把剩余空间分配给它们;这里不需要设备断点。
由内容触发的页面切换
页面结构确实需要离散变化时,再添加媒体查询。这个示例在窄视口保持单列,到正文和补充栏都能容纳时才切换为两列。
<meta name="viewport" content="width=device-width">
<main class="layout">
<article>Main report</article>
<aside>Filters</aside>
</main>
<style>
.layout {
display: grid;
gap: 1rem;
grid-template-columns: minmax(0, 1fr);
}
.layout > * {
min-width: 0;
padding: 1rem;
border: 1px solid #888;
}
@media (width >= 48rem) {
.layout {
grid-template-columns: minmax(0, 1fr) 16rem;
}
}
</style>
<script>
const columns = getComputedStyle(document.querySelector('.layout'))
.gridTemplateColumns.split(' ').length;
console.log(`640px viewport: ${columns === 1 ? 'stacked' : 'split'}`);
</script>640px viewport: stacked输出在 640px CSS 视口中执行得到。48rem 在默认根字号下为 768px,所以增强规则未匹配;DOM 中的正文仍在补充栏之前。
依据容器复用组件
卡片不知道自己位于整页、主栏还是侧栏,只读取最近的具名查询容器。断点属于组件的内容约束,因此组件移到另一页面模板时不必同步全局视口断点。
<section class="card-host">
<article class="profile">
<div class="avatar" aria-hidden="true">AL</div>
<div><h2>Ada Lovelace</h2><p>Computing notes and correspondence</p></div>
</article>
</section>
<style>
.card-host {
container: profile / inline-size;
width: 520px;
}
.profile {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
border: 1px solid #888;
}
.avatar {
display: grid;
place-items: center;
min-block-size: 8rem;
background: #ddd;
}
@container profile (width >= 30rem) {
.profile { grid-template-columns: 8rem minmax(0, 1fr); }
}
</style>
<script>
const columns = getComputedStyle(document.querySelector('.profile'))
.gridTemplateColumns.split(' ').length;
console.log(`520px container: ${columns} columns`);
</script>520px container: 2 columns查询阈值 30rem 在默认根字号下为 480px,所以宽度为 520px 的容器匹配横排布局。查询的是 .card-host,不能查询并同时依赖 .profile 自己因查询而改变的尺寸,否则可能形成不稳定反馈。
把用户偏好与设备尺寸分开
动态效果偏好与视口宽度无关。下面的页面在模拟减少动态效果时关闭过渡,但精确指针仍可保留悬停提示;核心按钮在所有组合下都存在。
<button class="save-button" type="button">
Save <span class="hint">(Ctrl+S)</span>
</button>
<style>
.save-button {
padding: 0.75rem 1rem;
}
.hint {
display: none;
}
@media (hover: hover) and (pointer: fine) {
.save-button:hover .hint {
display: inline;
}
}
@media (prefers-reduced-motion: no-preference) {
.save-button {
transition: transform 160ms ease-out;
}
.save-button:active {
transform: scale(0.98);
}
}
</style>
<script>
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const precise = matchMedia('(hover: hover) and (pointer: fine)').matches;
console.log(`reduced motion: ${reduced}; precise pointer: ${precise}`);
</script>reduced motion: true; precise pointer: false输出来自启用减少动态效果、且没有精确主指针的无头 Chromium。两项查询分别求值,说明代码不能根据屏幕宽度替用户推断偏好或输入方式。
陷阱
断点、容器与来源顺序
先选择查询坐标系
当条件描述整个浏览环境时,使用媒体查询。页面主导航、视口级双栏结构、打印样式和用户偏好都属于这个范围;组件内部不应通过全局页面宽度猜测自己有多少空间。
当条件描述组件被分配的空间时,使用容器查询。查询容器应由拥有该空间的包装元素建立,后代负责响应;不要仅为了查询而把 container-type 散落在每个元素上,因为它会改变尺寸包含行为和可查询祖先。
不需要离散状态时,两者都不用。Grid 的 auto-fit、Flexbox 换行、百分比、minmax() 和 clamp() 能表达连续约束,也会减少边界处规则冲突。查询负责布局模式变化,流式值负责模式内部伸缩。
从失败点得到断点
先用最窄的支持宽度和真实内容完成基础布局,再逐渐扩大容器。标题行长过长、控件之间出现多余空洞,或者两组内容终于可以并排时,记录那个约束变化点;断点应放在失败之前,而不是取最接近的设备预设值。
断点附近最容易暴露层叠错误。至少检查阈值前后各一个 CSS 像素,并改变根字号;如果查询用 rem 或 em,文字尺度变化会有意改变像素阈值。范围语法如 @media (40rem <= width < 64rem) 可以直接表达半开区间,避免相邻 min-width 与 max-width 在同一点重叠。
容器断点同样从组件内容得出。测试卡片时改变实际宿主宽度,而不是只拖动顶层视口;固定侧栏在宽视口中仍可能给卡片很窄的空间,这正是容器查询与媒体查询结果不同的场景。
把阅读顺序留在文档中
CSS 可以改变视觉位置,但屏幕阅读器和顺序键盘导航通常仍跟随 DOM。响应式布局应让来源顺序在每种模式下都讲得通,例如标题、正文、主操作、补充信息;这样单列和多列只是同一叙事的不同几何排列。
如果宽屏设计要求侧栏视觉上提前出现,可以把它放到另一列,但不要让键盘用户先穿过视觉上位于页面末端的控件。对交互内容逐项记录 DOM 序号、视觉坐标和 Tab 序号,这比只比较截图更容易发现错位。
建立约束测试矩阵
尺寸测试应覆盖断点本身,也要覆盖断点之间。选择能触发真实约束的用例:最短与最长内容、空状态、错误消息、缩放文字、窄容器、横竖方向,以及页面嵌入侧栏或对话框后的宽度。
能力测试与尺寸测试是两条轴。用键盘完成核心任务,分别模拟无悬停粗指针和有悬停精确指针,并启用减少动态效果;不要把某个视口预设当成这些能力的替身。
WCAG 2.2 的重排理解文档给出了一个明确审查目标:除二维布局等例外,内容在相当于 320 CSS 像素宽的条件下不应要求双向滚动。测试时还要定位具体溢出元素,因为给整个页面加 overflow-x: hidden 只会隐藏证据和可能仍需操作的内容。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题