移动端适配的标准答案只有一行代码:<meta name="viewport" content="width=device-width, initial-scale=1">,配合响应式布局(百分比、flex、媒体查询),就覆盖 95% 以上的适配场景。结论:viewport 标签要写,但只能写这两个属性,不要加 user-scalable=no 和 maximum-scale=1。
meta viewport 到底要怎么写?
结论:<meta name="viewport" content="width=device-width, initial-scale=1"> 这一行就是当前推荐写法,不需要再追加别的参数。
viewport meta 标签最早由 Apple 在 iPhone 3 时代引入 iOS Safari,用来控制「布局视口」(layout viewport,即 CSS 用来计算页面宽度的那块区域)的宽度。不加这行标签时,移动浏览器默认把布局视口设成一个固定值——iOS Safari 是 980px,部分 Android 浏览器是 980px 或 800px,页面会被整体缩小塞进屏幕,字小到看不清。
加上 width=device-width 后,布局视口宽度等于设备的理想视口宽度(ideal viewport,也就是 CSS 像素下的屏幕宽度),iPhone 15 上是 393px,Redmi K70 上是 393px 左右(取决于 DPR 换算)。这样写 CSS 时,1px 就等于设计稿上的 1 个 CSS 像素逻辑,媒体查询断点也才有意义。
完整可复制的写法:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
width=device-width 和 initial-scale=1 为什么建议都写?
结论:只写 width=device-width 在部分 iOS 场景下不生效,两个一起写才是稳的。
历史原因:早期 iOS Safari 在 iPad 横竖屏切换、以及部分页面加载时机下,单独设置 width=device-width 会被忽略,浏览器仍按 980px 渲染。加上 initial-scale=1 后,浏览器会把初始缩放比锁定为 1,强制布局视口与理想视口对齐,问题消失。
这不是「多写一个更保险」的玄学,而是 iOS 上真实存在过的渲染行为差异,所以主流 CSS 框架和 HTML5 Boilerplate 的默认模板都是两个属性一起给。
为什么不要写 user-scalable=no 和 maximum-scale=1?
结论:这两个参数会禁用用户双指缩放,违反 WCAG 2.1 的 1.4.4 条款(文本可缩放至 200%),并且在 iOS 10 及之后的 Safari 上已被直接忽略。
很多老教程教你写 width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no,目的是防止用户乱缩放导致布局错乱。但代价是:视力障碍用户无法放大文字,页面可访问性直接不合格。
更现实的是,iOS 10+ 的 Safari 已经明确忽略 user-scalable=no,你写了也没用,只会让 Android 端的部分浏览器(如 UC、老版 WebView)继续照做,造成行为不一致。
正确做法是:允许缩放,把布局本身做扎实——用 rem 或 clamp() 控制字号随屏幕变化,用 flex/grid 做自适应,而不是靠禁用缩放掩盖布局问题。
iPhone 上输入框为什么会被自动放大?
结论:当 input 的 font-size 小于 16px 时,iOS Safari 会在聚焦时自动放大整个页面,解决办法是把输入框字号设为 16px 以上。
这是 iOS 的一个硬编码规则,跟 viewport 设置无关,也不是 bug。现象是:用户点搜索框,页面「唰」地放大,再点别处又缩回去。
修复方式二选一:
- 直接给输入框
font-size: 16px(推荐,简单无副作用);
- 保持小字号,但用
transform: scale() 或调整 maximum-scale(后者又会回到禁用缩放的老问题,不推荐)。
注意:桌面端设计稿常把输入框写成 14px,上线移动端后要单独覆盖成 16px,否则用户体验会明显打折。
viewport-fit=cover 是干什么的?
结论:viewport-fit=cover 让页面延伸到 iPhone X 及之后机型的刘海和底部横条区域,配合 env(safe-area-inset-*) 使用。
不加这个属性时,iOS 会把页面限制在安全区域内,两侧留白。加了之后,页面铺满整屏,但你需要自己避让:
body {
padding-bottom: env(safe-area-inset-bottom); /* 底部 Home 指示条 */
}
.header {
padding-top: env(safe-area-inset-top); /* 顶部刘海 */
}
env() 是 CSS 环境变量函数,Chrome 69+、Safari 11.1+ 支持,不支持的浏览器会自动忽略该行,不会出错。
100vh 在移动端为什么不准?
结论:移动浏览器地址栏收起/展开会改变可视高度,100vh 取的是「地址栏收起时」的最大高度,导致页面底部被裁掉或出现滚动条。
Chrome 108(2022 年 12 月)起支持动态视口单位,用 100dvh(dynamic viewport height)替代 100vh 可以实时跟随地址栏变化。另外还有 svh(最小高度,地址栏展开时)和 lvh(最大高度,地址栏收起时)。
.hero {
height: 100vh; /* 旧浏览器回退 */
height: 100dvh; /* 现代浏览器用动态高度 */
}
Safari 15.4+、Chrome 108+、Firefox 101+ 均支持,覆盖率已经足够,可以直接用,只要保留一行 100vh 做降级。
一句话总结
viewport 标签只写 width=device-width, initial-scale=1,需要全面屏再加 viewport-fit=cover;不要禁用缩放;输入框字号给 16px;高度用 dvh 替代 vh;剩下的适配交给响应式布局。