做前端的这些年我见过太多一套页面走天下的项目也见过不少到了移动端就乱成一锅粥的页面。CSS 媒体查询Media Queries就是解决这类问题的核心工具它让我们能针对不同的屏幕尺寸、设备特性去定义不同的样式规则让同一套页面在手机、平板、电脑显示器上都呈现合理的布局。这篇文章不打算讲大而全的理论就结合我实际项目里的经验把媒体查询的语法、实战写法、断点选择、坑点排查完整地过一遍。适合刚入门前端、准备做响应式布局的朋友也适合想系统梳理媒体查询用法的同学照着敲一遍基本就能上手。1. 媒体查询到底解决什么问题1.1 响应式设计的最小工作单元Web 页面天生是流动的文字可以换行块元素默认占满整行但一旦你给容器写了固定宽度比如width: 1200px那么在 375px 宽的手机上这个容器就直接溢出了横向滚动条就这样产生了。用户浏览时还得左右拖动体验非常糟糕。媒体查询做的事其实很简单它允许你根据设备的某些特征最常见的是视口宽度来决定是否应用某组样式。比如.container { width: 1200px; } media (max-width: 1200px) { .container { width: 100%; } }当视口宽度小于等于 1200px 时.container的宽度被覆盖为 100%页面就能自适应了这就是一个最基础的响应式方案。有人可能会问这不就是判断屏幕大小然后改样式吗对但它的价值在于这一整套判断逻辑是浏览器原生支持的写在 CSS 里就能生效不需要 JavaScript 参与。这意味着页面在首次渲染时浏览器就能根据当前设备特性直接应用正确的样式没有脚本阻塞也没有闪一下再变化的尴尬。你可以把它理解成一个分流闸门水流视口条件到了这个闸门会自动走向对应的渠道样式规则整个过程无需外部指令。1.2 为什么是按宽度区间而不是按设备型号很多初学者容易陷入一个误区以为做响应式就是要兼容 iPhone X、iPhone 14、iPad Pro、MacBook 这些具体型号。实际上设备型号是在快速迭代的你今天为 iPhone 14 单独写了一套样式明天 iPhone 15 上市了屏幕宽度稍微变一点你的布局可能又出问题那就要无穷无尽地补样式。媒体查询的设计哲学是按视口宽度区间去匹配而不是按设备型号去匹配。你要考虑的是一系列宽度范围手机竖屏通常小于 768px、平板768px 到 1024px 之间、桌面大于 1024px等等。某个具体的设备落在哪个区间就应用哪个区间的样式。这种思路的好处是即便未来出了新设备只要它的宽度落在已有区间里页面基本不会出大问题。真正决定页面长什么样的不是这是什么牌子而是它的屏幕有多宽、是什么形态。1.3 没有媒体查询的日子是怎么过的在媒体查询普及之前前端做多端适配主要靠两招一是做两套页面PC 一套、移动端一套域名或者子路径分开成本和维护量翻倍二是用 JavaScript 检测屏幕宽度动态改样式或者加载不同的 CSS 文件。前者的问题是内容不同步改一处要动两边后者的问题是需要等脚本执行完才能确定样式首屏可能出现短暂的无样式闪烁而且逻辑一旦复杂代码就非常难维护。媒体查询把条件判断下沉到了 CSS 层面让样式自己知道在什么环境下该怎么表现。这种声明式的写法既减少了 JavaScript 的负担也让样式逻辑和布局逻辑内聚在同一个地方。你打开一个 CSS 文件从上往下读就能知道这个页面在不同宽度下的完整形态这对团队协作和维护来说意义巨大。2. 媒体查询的核心语法与配置2.1 最基本的写法媒体查询的语法结构是media后跟一个或多个条件条件成立时内部花括号里的样式才会生效。最常用的两个形式media (max-width: 768px) { /* 视口宽度 768px 时生效 */ } media (min-width: 769px) { /* 视口宽度 769px 时生效 */ }这里要注意max-width和min-width的边界问题。max-width: 768px包含 768px 这一档min-width: 769px从 769px 开始。如果你写max-width: 768px和min-width: 768px两个区间那么在正好 768px 的设备上两组样式都会命中。如果规则优先级相同后面写的规则会覆盖前面的这个特性既是工具也是坑。实际开发中我建议两个区间之间不要留断档也不要重叠。比如用max-width: 767px和min-width: 768px做无缝衔接或者用max-width: 768px和min-width: 769px。不要让两组规则在某个宽度上同时生效除非你有明确的覆盖意图否则很容易出现我在电脑上调试得好好的缩到手机宽度突然样式乱了的诡异情况。2.2 逻辑操作符媒体查询支持三种逻辑操作符and、not、only以及用逗号实现的或逻辑。/* 并且宽度在 600px 到 900px 之间且是横屏 */ media (min-width: 600px) and (max-width: 900px) and (orientation: landscape) { /* 样式 */ } /* 或小于 600px 或者大于 900px */ media (max-width: 600px), (min-width: 900px) { /* 样式 */ } /* 否定不是打印设备 */ media not print { /* 样式 */ }逗号分隔的多个条件只要有一个成立整条规则就生效等价于或逻辑。这个在区分横屏、竖屏时特别好用比如平板竖屏是一种导航布局横屏是另一种用逗号或者and组合都能实现。only操作符主要是为了隐藏那些不识别媒体查询的老旧浏览器对现代浏览器来说加不加only效果基本一样但如果你需要兼容很老的环境写上会更保险。not的优先级需要注意它会作用于整条媒体查询而不是单个条件所以使用时最好把整条条件用括号包起来避免预期之外的逻辑。2.3 常用的媒体特性除了宽度媒体查询还能检测很多其他特性。我列几个实际用得上的媒体特性作用示例场景width/min-width/max-width视口宽度响应式布局的核心height/min-height/max-height视口高度首屏高度适配orientation横屏 / 竖屏平板、手机旋转适配aspect-ratio视口宽高比特殊形状布局resolution屏幕分辨率高清屏图片适配prefers-color-scheme系统深色 / 浅色模式暗色模式适配prefers-reduced-motion用户是否偏好减少动画无障碍优化hover是否支持悬停区分触屏和鼠标设备其中prefers-color-scheme和prefers-reduced-motion是近几年关注度比较高的。前者能做暗色模式不用单独引一整套 CSS只要在媒体查询里覆盖变量的值就行后者能帮对动画敏感的用户关掉花哨的动效算是低成本的无障碍优化。hover特性也很有意思它能帮你判断用户设备是否支持鼠标悬停从而决定某个交互要不要做成移上去才显示的形态——触屏设备上根本没有 hover如果把关键信息藏进 hover 里手机用户就找不到了。2.4 在 HTML 中引入媒体查询的其他方式除了在 CSS 文件里写media规则你还可以在 HTML 层面使用媒体查询。一种是通过link标签的media属性按条件加载不同的样式文件link relstylesheet hrefbase.css link relstylesheet hrefmobile.css media(max-width: 768px)这种方式在移动端优先的架构里偶尔会用到但我个人不太推荐把它作为主力方案。原因很简单每个样式文件都是一次 HTTP 请求文件拆得太碎会影响加载性能而且mobile.css和base.css之间的覆盖关系隔了两个文件调试时要在 DevTools 里来回切排查问题的成本变高了。我更倾向于把同一组件的所有样式放在一起用media块做分区这样读代码时上下文是连续的。不过如果你面对的是一个超大项目且某个端比如打印样式完全独立用link mediaprint单独加载打印样式倒是一个干净的做法。3. 实战用法与完整示例3.1 响应式导航栏改造导航栏是每个网站几乎都会有的组件。桌面端通常是横向排列的菜单到了手机端空间不够就变成了汉堡菜单。完整的汉堡菜单交互需要 JavaScript 配合这里先展示纯 CSS 能完成的两档切换。假设 HTML 结构是这样header classsite-header a href/ classlogoLogo/a nav classnav a href# classnav-link首页/a a href# classnav-link产品/a a href# classnav-link关于/a a href# classnav-link联系/a /nav /header桌面端样式.site-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 32px; } .nav { display: flex; gap: 24px; }移动端让导航变成纵向排列并且占满整个头部宽度media (max-width: 640px) { .site-header { flex-direction: column; align-items: stretch; padding: 16px; } .nav { flex-direction: column; gap: 8px; margin-top: 12px; } .nav-link { padding: 12px 0; border-bottom: 1px solid #eee; } }这里面的关键点在于桌面端和移动端共用同一套 HTML只是通过媒体查询切换了布局方式。flex 布局本身就是响应式的但flex-direction从row变成column视觉上就完全是两种菜单了。如果你要在手机端做成点击汉堡按钮才展开的交互就需要在小屏幕下默认隐藏.nav再用一个复选框或者按钮配合 JS 控制展开样式部分依然离不开媒体查询的辅助。我曾经在一个项目里偷懒只在桌面端隐藏了汉堡按钮忘了在移动端隐藏横向菜单结果手机用户看到的是一个横向溢出、被截断的菜单列表排查了很久才发现是媒体查询条件写反了。3.2 卡片网格从四列收缩为一列商品列表、文章列表这类卡片布局是媒体查询最经典的用武之地。桌面端放四列平板放两列手机放一列这在电商详情页、博客列表里太常见了。div classcard-grid div classcard卡片内容/div div classcard卡片内容/div div classcard卡片内容/div div classcard卡片内容/div /div.card-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } media (max-width: 1024px) { .card-grid { grid-template-columns: repeat(2, 1fr); } } media (max-width: 640px) { .card-grid { grid-template-columns: 1fr; } }三个断点四列、两列、一列写起来很直白。我实际工作中的习惯是先写桌面端最大断点下的样式然后再用max-width逐级往下覆盖。这个顺序比较直观因为桌面端往往是信息量最大的形态先把它定好再思考当宽度缩小时哪些元素需要调整。也有人习惯移动端优先用min-width从最小宽度往上写。两种思路都可以关键是保持全项目风格统一。混着写是最痛苦的一会儿max-width一会儿min-width维护的时候思路得不停切换而且很容易在一个断点边界上出问题。选用哪种策略应该在项目开始时就跟团队定好然后写进规范里。3.3 表单和表格的响应式处理表单是响应式里容易被忽略的一块。桌面端的表单往往是一行放多个输入框到了手机端一行只能放一个。这里有一个非常实用的技巧用flex-wrap加媒体查询控制每个表单项的最小宽度。.form-row { display: flex; flex-wrap: wrap; gap: 16px; } .form-row .field { flex: 1 1 200px; } media (max-width: 640px) { .form-row .field { flex: 1 1 100%; } }在flex: 1 1 200px的作用下每个表单项的最小宽度是 200px容器宽度足够时它们自动排成一行容器不够宽时会自动换行。这个方案甚至不依赖媒体查询就能完成一部分自适应媒体查询只是在小屏上把每个字段强制拉满整行让表单的阅读顺序更清晰。表格的响应式处理更麻烦。一个五六列的宽表格在手机上直接显示必然会溢出。常见做法是用媒体查询在窄屏下把表格改成卡片式每一行变一张卡片表头和数据纵向排列。我一般用 CSS 的伪元素和>media (max-width: 640px) { table { display: block; } tbody { display: block; } tr { display: block; border: 1px solid #ddd; margin-bottom: 12px; padding: 8px; } td { display: flex; justify-content: space-between; padding: 8px; } td::before { content: attr(data-label); font-weight: bold; } }对应的 HTML 需要在每个td上写>.card { background: red; } media (max-width: 768px) { .card { background: blue; } }这段代码没问题background会被覆盖成蓝色。但你把顺序换一下media (max-width: 768px) { .card { background: blue; } } .card { background: red; }在小于 768px 的屏幕上背景还是红色的因为后面的.card { background: red; }优先级相同、并且位置靠后覆盖了媒体查询里的规则。这不是媒体查询的问题是 CSS 层叠的基本规则。很多人以为媒体查询里的样式一定优先其实是错的。要避免这个问题最简单的方法是养成默认样式在前媒体查询覆盖规则在后的书写习惯。同时每个媒体查询块内部也要按照从宽到窄或从窄到宽的顺序排列形成一个清晰的层叠链条。4.2 选择器优先级叠加的问题再举一个场景。如果你在媒体查询里用了更高优先级的选择器那么即使它写在前面也能赢过后面低优先级的同名样式media (max-width: 768px) { .container .card { background: blue; } } .card { background: red; }小屏幕上蓝色生效因为.container .card是两个类选择器的组合优先级比单个.card高。理解这个逻辑之后排查怎么我改了样式不生效的问题时你应该先检查是不是有更高优先级的选择器在别处把这个值定了。我在团队里给新人的建议是媒体查询里不要滥用!important。遇到覆盖问题优先检查选择器优先级和书写顺序实在搞不定再考虑!important。滥用!important会造成一种混乱局面——谁用了它谁说了算而且一旦用了以后要覆盖同一个属性就只能也加!important维护成本瞬间升高。CSS 的级联机制本来就是用来让样式有序叠加的不要主动破坏它。4.3 移动端优先和桌面端优先的写法差异桌面端优先和移动端优先这两种写法在和优先级机制结合时会产生不同的心智负担值得单独说一下。桌面端优先就是先用普通规则写桌面样式再用media (max-width: ...)去覆盖小屏样式。这种写法符合大多数人先大后小的直觉而且当你主要工作在电脑上开发时默认样式就是你在桌面看到的样子调试很直接。移动端优先则是先用普通规则写移动端样式再用media (min-width: ...)去增强大屏样式。这种写法强制你在最受限的环境下思考什么是核心功能内容优先级会自然浮出来。Bootstrap 4 之后官方就是移动端优先的思路。两种都可行但要注意一旦选定全程保持一致。如果你混着写比如有些组件用max-width覆盖有些用min-width增强那么在某个中间宽度上你可能要同时查看两套媒体查询规则才能推理出最终效果这个心理负担在复杂项目里会被放大很多倍。5. 常见问题与排查技巧实录5.1 移动端横向滚动条的元凶这是出现频率最高的一个问题页面在手机上有横向滚动条排查方向是找出超出视口的元素。常见的原因有固定宽度的容器没有加响应式规则某个子元素设置了white-space: nowrap不换行表格内容太宽图片原始尺寸太大且没有max-width: 100%排查方法在手机模拟器里或者把浏览器窗口缩窄到手机宽度然后从右往左拖动横向滚动条观察是哪个元素一直顶着屏幕。更快捷的办法是打开 DevTools在 Elements 面板里选中body看它的子元素里有没有实际宽度超过视口宽度的。如果实在找不到可以在 Console 里跑一段脚本document.querySelectorAll(*).forEach(el { const rect el.getBoundingClientRect(); if (rect.right window.innerWidth 1) { console.log(el, rect.right, window.innerWidth); } });这段代码会打印出所有超出视口的元素一抓一个准。找到元凶之后针对它写媒体查询或者干脆给所有图片都加上max-width: 100%; height: auto;这个习惯能避免一大半横向溢出问题。5.2 DevTools 里调试媒体查询的正确姿势Chrome DevTools 的 Device Toolbar 是调试响应式布局最重要的工具。在里面可以直接切换各种预设设备尺寸也可以手动拖动视口宽度。我调试时通常这样做先选择一个接近目标设备的宽度比如 375px把页面过一遍看有没有明显的布局问题。遇到问题不要急着改代码先把视口宽度停留在出问题的那个值上然后打开 Elements 面板。这时如果某条媒体查询的规则处于生效状态样式面板里会明显标出来。你还可以在 CSS 面板里直接编辑媒体查询条件实时看效果改满意了再回代码里同步。有一个小技巧在 Device Toolbar 的尺寸输入框里可以直接输入任意像素值也可以用鼠标拖动视口边界边拖边看布局变化。多拖动几次你就能直观地感受到在哪个宽度区间布局最难受这个感受比任何断点表格都靠谱。另外如果你在 DevTools 里看到某条规则被划掉了不要直接删掉它鼠标悬停在上面看看它是因为什么条件不满足而被忽略的这能帮你定位我明明写了为什么没生效的问题。5.3 打印样式要不要用媒体查询媒体查询的print媒体类型可以单独给打印页面写样式这个场景经常被忽略。公司官网的详情页、后台系统的报表页用户是真的会按 CtrlP 打印的。如果你不写打印样式打印出来的页面很可能把导航、侧边栏、广告位全带出来了甚至还带着深色背景打印出来一片黑。一个基本写法media print { .site-header, .site-footer, .sidebar { display: none; } .main-content { width: 100%; } body { background: #fff; } }配合-webkit-print-color-adjust: exact和print-color-adjust: exact还可以控制打印时是否保留背景色。这块虽然不是媒体查询的重点用法但做企业项目时客户突然提出我要把页面打印成 PDF你能快速搞定印象分会加不少。5.4 测试设备不在身边怎么办大部分团队的设备墙没有那么多真机这时候要善用 DevTools 的模拟功能。但模拟器终究是模拟器它在 CSS 像素宽度上靠得住在真实的物理渲染、字体、触摸行为上会有差异。我的经验是布局层面的问题模拟器完全够用字体渲染和细节间距的问题尽量找个真机验证一下。如果连真机都没有还有一个笨办法把同一个 URL 发给同事让他在手机上打开然后截图给你看。对比几个不同宽度的截图基本也能定位问题。当然最稳妥的还是买几台覆盖主流屏幕尺寸的测试机尤其是 375px 和 390px 这两档 iPhone 尺寸以及 360px 左右的主流 Android 尺寸它们才是移动端流量的主力。6. 进阶技巧与扩展思路6.1 用容器查询替代部分媒体查询媒体查询只能检测视口的宽度它管不到某个组件容器的宽度。比如一个卡片组件放在侧边栏时宽度只有 300px放在主内容区时宽度有 900px你想让它在窄容器里自动变成纵向排列媒体查询做不到因为它俩处于同一个视口宽度下。容器查询Container Queries就是为了解决这个问题而生。思路是给父容器声明一个container-type组件的样式就可以基于这个容器的宽度来响应而不是基于视口。.card-container { container-type: inline-size; } container (max-width: 400px) { .card { flex-direction: column; } }容器查询和媒体查询并不是替代关系而是分工不同页面级布局用媒体查询组件级布局用容器查询。目前主流浏览器都支持容器查询了做组件库或者复杂后台系统的朋友值得认真学一下。如果你只是做简单的营销页媒体查询已经足够不必为了炫技引入新概念。6.2 结合 CSS 变量管理断点媒体查询里可以直接使用 CSS 变量这能有效减少重复代码。比如你想在移动端统一收紧页面边距:root { --page-padding: 32px; } media (max-width: 768px) { :root { --page-padding: 16px; } } .container { padding: 0 var(--page-padding); }这样一来你只需要在媒体查询里改变量的值所有用到--page-padding的元素都会跟着变。比起在每个元素里单独写媒体查询规则这种做法的维护成本低很多特别是项目里有很多区块都需要统一间距时。我用这个思路管理过一套后台系统的主题变量包括边距、字体大小、圆角半径、阴影强度所有响应式变化全部集中在几个媒体查询块里其他地方根本不用写媒体查询。团队其他人接手时也很快就能看懂因为样式的变化逻辑一眼就能扫完。6.3 新特性与兼容性处理媒体查询本身不会给页面增加额外负担它只是条件样式浏览器只应用命中的那部分规则。真正影响性能的是你在媒体查询内部写了什么。比如在移动端媒体查询里加载了大尺寸背景图移动端用户虽然看到了适配的布局但图片还是照样下载这点需要配合图片的srcset或者picture元素一起控制。兼容性方面min-width、max-width这类基础写法在 IE9 以上都支持prefers-color-scheme、prefers-reduced-motion这类新特性老浏览器不支持也不会有副作用最多就是不生效样式会回退到默认状态。所以开发时可以先写基础样式再把增强规则包在媒体查询里老浏览器直接忽略掉就行。还有一个值得养成的习惯媒体查询里的规则尽量做得小而局部。不要在一个媒体查询块里同时改几十个组件的样式那样一是难读二是容易误伤。每个组件自己负责自己的响应式页面级别的布局调整单独写整体会清晰很多。最后分享一个我自己的习惯每个项目的全局样式文件里我会把媒体查询集中放在文件末尾并按照从大到小或从小到大的宽度顺序排列配上注释说明每个断点的用途。这样看起来可能多花了一点时间但是当项目上线几个月后你要回头改一个响应式样式时打开样式文件能第一时间定位到对应断点而不是在几千行 CSS 里翻来找去。这个习惯帮我节约的时间远比当初随手排一下顺序花的时间多。
