移动端适配入门:做前端页面时,别把响应式理解成只改宽度
同一份 CSS,height: 100vh 写的全屏弹层,在 Chrome 设备模拟器里严丝合缝,一到真机 Safari 上底部就被地址栏盖掉一截。同一个固定底部按钮,页面刚进来时好好的,用户往下滚一段、地址栏收起,它的位置又变了。同一个输入框,在桌面浏览器聚焦什么都不发生,在 iOS 上聚焦却把整页强行放大。
这些不是 bug,是移动端和桌面端在几个底层机制上根本不一致:视口高度是会动的、系统 UI 会占用可视区域、触屏没有 hover、软键盘弹起会挤压布局。"把宽度缩小"只是响应式适配里最容易的那一步,同一段代码在不同设备、不同状态下行为会分叉才是要花功夫的地方,你得知道它为什么分叉。屏幕窄只是最表面那一层。
下面这些坑,几乎每一个都能追到某个"同一页面不同设备行为不一致"的机制矛盾上。
先认清移动端多出来的那几个变量
桌面端的可视区域基本是稳定的:一个窗口,宽高你能拿准。移动端不是。浏览器地址栏会随滚动收起或展开,底部操作栏、刘海屏安全区都在动态占用空间。一个在桌面端固定高度很舒服的区域,放到手机上可能刚好挡住关键按钮——不是你写错了,是可视区域本身在变。
我现在会把移动端页面按"三个状态"分别看:页面初次进入时、用户滚动后地址栏收起时、输入框聚焦键盘弹出时。很多布局在第一种状态没问题,到后两种就露馅。尤其是固定底部按钮和全屏弹窗,必须拿真机在这三种状态下都过一遍。设备模拟器只给你一个大概轮廓,那几个会动的变量,模拟器根本不会帮你动。
除了视口在动,还有触控优先、系统字体缩放、不同像素密度、弱网这些变量。同一个布局在电脑上"信息丰富",在手机上可能就是"阅读压力很大"。
响应式不只是改列数
做响应式,最基础的动作是三列变两列、两列变一列。这是基础,但不够。移动端还要考虑字体大小是否适合阅读、行高是否舒适、点击区域是否够大、信息层级是否需要重组。有些桌面端布局不是缩小后继续摆放,而是应该重新组织内容顺序——桌面端左侧筛选、右侧列表,移动端可能更适合把筛选收进抽屉,列表占主要区域。响应式不是机械压缩,而是重新安排优先级。
我写断点的习惯也变了。早些年喜欢一上来堆 @media (max-width: 768px) 这种从大往小覆盖,结果全是层层覆盖的样式,越改越乱。后来改成移动端优先:默认样式就是手机的单列状态,往上用 min-width 渐进增强。默认那套最简单,加进去的每一条都是"屏幕更宽了我才需要的东西",逻辑顺很多。
1/* 默认就是移动端,单列 */ 2.layout { 3 display: flex; 4 flex-direction: column; 5 gap: 16px; 6} 7 8/* 屏幕够宽了,才变成左右分栏 */ 9@media (min-width: 768px) { 10 .layout { 11 flex-direction: row; 12 } 13 .sidebar { 14 flex: 0 0 240px; 15 } 16}
断点的值别去硬背那些"常见机型分辨率"。机型每年都在变,盯着具体尺寸只会越列越多。按内容来定断点——什么时候这行字开始挤了、什么时候卡片排不下两列了,就在那里加一个断点。内容驱动的断点比设备驱动的断点稳定得多。
这里还藏着一个媒体查询本身的机制矛盾:同一个卡片组件,放在主内容区和放在侧栏里,屏幕宽度完全一样,但组件实际可用宽度差很多。媒体查询看的是屏幕宽度,组件真正关心的却是"我自己所在的容器有多宽",这两者对不上,就会出现"屏幕够宽但组件被挤扁"的怪现象。容器查询正是冲着这个矛盾来的:
1.card-list { 2 container-type: inline-size; 3} 4 5@container (min-width: 520px) { 6 .card { 7 grid-template-columns: 160px 1fr; 8 } 9}
到 2023 年,容器查询已经适合在现代浏览器里作为渐进增强使用(Chrome 从 105 起支持),但我不会用它直接替代所有媒体查询。要先看目标用户的浏览器范围,再决定是默认上容器查询,还是给核心布局保留媒体查询兜底。它提供的是比"全站一套断点"更细的适配思路。
viewport 是一切的基础
移动端页面通常需要设置 viewport:
1<meta 2 name="viewport" 3 content="width=device-width, initial-scale=1" 4>
width=device-width 表示布局视口跟设备宽度一致,initial-scale=1 表示初始缩放为 1。不设它,很多手机会按一个默认的 980px 宽来渲染再整体缩小,那正是"电脑上正常、手机上一切都变小"的老问题源头。
不要轻易用 user-scalable=no 禁止缩放。对视力不好或需要放大阅读的用户,缩放是重要能力。除非是画布、地图这类自己接管了缩放手势的强交互页面,否则不建议禁用。而且 iOS Safari 从某个版本起就直接无视 user-scalable=no 了,你写了也未必生效,纯属给自己埋无障碍隐患——这本身又是一个"同一段代码不同设备行为不一致"的例子:安卓上它可能还生效,iOS 上直接被忽略。
这里有个老坑:iOS 上当输入框 font-size 小于 16px 时,聚焦会触发整页自动放大,很突兀。我见过有人为治这个去禁缩放,其实根本不用——把输入框字号保证在 16px 以上就行。
1input, 2textarea, 3select { 4 font-size: 16px; /* iOS 上小于这个值,聚焦时页面会被强行放大 */ 5}
图片和媒体内容一定要自适应
图片是移动端最容易出问题的内容之一:固定宽高导致溢出、多图布局在小屏里过挤、大图加载太慢。所以移动端里媒体内容通常要保证宽度跟容器走、高度按比例自适应、不在小屏里强行保留复杂排版。基础样式是:
1img, 2video { 3 max-width: 100%; 4 height: auto; 5}
需要固定比例时用 aspect-ratio,避免图片加载前后页面高度跳动:
1.cover { 2 aspect-ratio: 16 / 9; 3 object-fit: cover; 4 width: 100%; 5}
大屏封面直接给手机加载原图,浪费流量也拖慢首屏。我现在的默认做法是用 <img> 的 srcset + sizes 让浏览器自己挑合适的尺寸,再配原生的 loading="lazy",大部分场景就够,不用动不动上 JS 懒加载库:
1<img 2 src="cover-800.jpg" 3 srcset="cover-400.jpg 400w, cover-800.jpg 800w, cover-1200.jpg 1200w" 4 sizes="(max-width: 600px) 100vw, 600px" 5 loading="lazy" 6 decoding="async" 7 alt="文章封面" 8>
sizes 我理解错过好久——它不是图片实际尺寸,而是告诉浏览器"这张图在布局里大概占多宽",浏览器再结合 DPR 去 srcset 里挑一张。写对之后,同一张封面在手机上可能只下了 400 那张,流量省一大截。对体积特别敏感,还可以用 <picture> 给支持的浏览器优先发 WebP/AVIF,老浏览器回退到 JPG。
触控体验和鼠标体验不是一回事
桌面端很多交互默认依赖 hover,移动端根本没有 hover 这个自然前提。这是个硬性机制差异,不是细节。它意味着:关键信息不能只放在 hover 态里、按钮不能太小、要给手指点击留足空间。在手机上,"能点到"本身就是体验的一部分。
点击区域别只看图标大小,要看可触达区域。一个 16px 图标可以放在 44px 的按钮区域里:
1<button class="icon-button" type="button" aria-label="关闭"> 2 × 3</button>
1.icon-button { 2 min-width: 44px; 3 min-height: 44px; 4}
hover 态里的提示要有移动端替代方案。桌面端 hover 显示操作按钮,移动端可以直接展示常用操作,或者通过更多按钮打开菜单。
还有个反复坑我的机制矛盾:桌面端拿 :hover 写按钮反馈,触屏上点完之后 hover 态会"粘"住,按钮一直停在高亮状态,得用 :active 才对。同一句 :hover,鼠标设备上是"悬停时",触屏上却变成"点完后一直保持",行为完全对不上。触屏点击默认还有一层灰色高亮,按起来有种廉价感,我一般顺手关掉,自己用 :active 控制:
1button, 2a { 3 -webkit-tap-highlight-color: transparent; 4} 5 6.btn:active { 7 opacity: 0.7; 8}
滚动和手势冲突也要注意。横向滑动卡片、地图、轮播图放在纵向页面里,没处理好 touch-action,用户会觉得页面很"粘"。复杂手势组件最好明确告诉浏览器哪些手势由页面处理、哪些由组件处理:
1.horizontal-list { 2 touch-action: pan-x; 3}
这属性不是万能药,但比在 JavaScript 里乱拦 touchmove 更可控。
文本排版比你想的更重要
移动端不是桌面端缩小版,长文本阅读尤其能体现。标题过大、行距太紧、段落过长,阅读压力会明显增加。所以移动端更值得关注标题层级是否清楚、每行文字长度是否合适、行距是否足够、段间距是否有节奏。
一个容易忽略的点是行长和留白。桌面端可以限制正文最大宽度,移动端则要保证左右留白足够:
1.article { 2 padding-inline: 16px; 3 line-height: 1.75; 4}
不要让文字贴着屏幕边缘。边距看似只是视觉细节,但明显影响阅读舒适度。
安全区和固定底部按钮
有些手机底部有系统手势区域,固定底部按钮不处理可能被遮挡。用安全区变量:
1.bottom-bar { 2 position: fixed; 3 right: 0; 4 bottom: 0; 5 left: 0; 6 padding-bottom: calc(12px + env(safe-area-inset-bottom)); 7}
页面有底部固定操作栏,主体内容也要留出对应空间,避免最后一段被挡住。要注意 env(safe-area-inset-*) 想生效,viewport 里得加 viewport-fit=cover,否则页面不会延伸到安全区,这几个变量取出来全是 0:
1<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
再回到开头那个 100vh 的矛盾。移动端浏览器里地址栏随滚动收起或展开,而 100vh 算的是地址栏收起后的最大高度,所以一进页面经常底部被地址栏盖掉一截,做全屏弹层时尤其明显——这就是"同一份高度值在桌面稳定、在移动端会漂"的根源。到 2023 年我会优先写 100dvh(动态视口高度)并保留 100vh 兜底,让支持的浏览器跟着地址栏实时变:
1.fullscreen-modal { 2 height: 100vh; /* 老浏览器回退 */ 3 height: 100dvh; /* 跟随地址栏变化的真实可视高度 */ 4}
dvh 还比较新,我会保留 vh 那行做回退,不敢直接只写 dvh。搭配它的还有 svh(小视口,地址栏展开时)和 lvh(大视口,地址栏收起时),需要精确控制某个状态时用得上。
DPR 和 1px 边框
移动端设备像素密度不同,同样的 CSS 像素在不同屏幕上显示效果可能不一样。设计稿常要求"1px 边框",在高 DPR 屏幕上看起来可能偏粗或偏细——同一句 border: 1px,1 倍屏和 3 倍屏落到物理像素上完全不是一回事,这也是设备差异带来的表现分叉。
多数情况直接用 CSS 边框即可,不必过度纠结。但对视觉精度要求高的组件,可以用 transform 缩放伪元素实现更细边框:
1.hairline { 2 position: relative; 3} 4 5.hairline::after { 6 position: absolute; 7 inset: 0; 8 border: 1px solid #e5e7eb; 9 content: ""; 10 pointer-events: none; 11 transform: scaleY(0.5); 12 transform-origin: top; 13}
这类技巧按需使用,不要让简单页面变复杂。
表单和软键盘
移动端表单还要考虑软键盘。输入框聚焦后可见区域变小,底部按钮可能被顶起或遮挡。合理设置输入类型能改善体验:
1<input type="tel" inputmode="numeric" placeholder="请输入手机号"> 2<input type="email" placeholder="请输入邮箱">
type 和 inputmode 是两件事,搭配着用更稳:type="tel" 决定语义和校验,inputmode="numeric" 决定弹哪种键盘。输验证码我会用 inputmode="numeric" 直接弹纯数字键盘,比 type="number" 那个带加减小箭头的体验好得多。再配 autocomplete,短信验证码还能触发系统自动填充:
1<input 2 type="text" 3 inputmode="numeric" 4 autocomplete="one-time-code" 5 maxlength="6" 6 placeholder="请输入验证码" 7>
软键盘弹起把布局顶乱也是典型的设备行为不一致:安卓上键盘弹出默认会压缩 viewport,固定底部的提交按钮可能被顶到键盘上方,也可能直接被盖住,各家浏览器行为还不统一;iOS 上键盘则是盖在页面之上、不压缩 viewport,同一个固定按钮的表现又不一样。我现在的经验是:移动端提交按钮尽量别用 position: fixed 钉在底部,让它跟着表单走、随内容滚动,反而最省心。真要做悬浮按钮,新一点的方案可以监听 visualViewport 的 resize 事件拿到键盘弹起后的真实可视高度手动调位置,但这套兼容性得自己多在真机上验。
适配是持续校正,不是一次性工作
页面开发里常见的情况是第一次上线时对几个常见机型调一下,看着差不多就结束了。但不同尺寸、不同系统、不同字体缩放设置都会影响展示,适配更像持续校正。
测试别只看一种手机宽度,至少覆盖小屏手机、常见大屏手机、横屏、系统字体放大、弱网加载,项目支持深色模式的话也要看。很多问题在开发者工具里看不出来,真实设备上的地址栏收缩、软键盘、滚动惯性都会影响体验。
把响应式理解成"重新组织体验",比理解成"改几个宽度断点"更接近实际开发。 那些真机上才暴露的坑,追到底几乎都是同一段代码在不同设备、不同状态下行为分叉。做移动端适配时先看内容优先级和操作路径,再看布局断点。宽度只是结果,真正要保证的是用户在小屏、触控和复杂环境下仍然能顺利阅读和操作。