一不留神,CSS 新单位多了好几十个:svh、dvh、lvh、cqw、cqi、cqh、lh、rlh、cap、ic……光看名字就够劝退一半人。但这些单位不是单纯凑数。真正的问题起点,是移动端那个让人头疼的100vh。我曾经为了一个全屏引导页,在真机上反复调高度,最后发现问题不是布局,而是“视口”在移动浏览器里本来就是动态的。地址栏一收,工具栏一弹,100vh 对应的高度就变了。
我对这些新单位的态度很明确:几十个里,值得认真用起来的就两组——动态视口单位和容器查询单位,它们会改变你对响应式布局的理解方式。另外几个字体相关单位属于特定场景的补充,能用,但不必全记。这篇文章,我会把“为什么会出现这些单位”“每个单位适用在哪”“落地时怎么写回退”讲清楚,最后给你一个排查流程,免得新单位写进项目后半天查不出问题。
1. 别再拿100vh当一屏:动态视口单位才是移动端答案
1.1 地址栏一收一缩,100vh 就变了
在桌面浏览器里,100vh几乎等于“浏览器可视高度”,用得很顺手。到了手机端,事情变复杂了。Safari、Chrome 等浏览器的地址栏、底部工具栏会随着滚动收起或展开,导致“视口”的实际高度一直在变化。
很长一段时间里,移动端浏览器为了降低页面抖动,会把100vh解析成地址栏收起之后的最大高度,而不是用户当前真正能看到的高度。这就带来两个经典问题:
- 全屏弹层底部被地址栏挡住;
- 滚动过程中,背景高度突然跳了一下。
过去最常见的粗暴方案是用window.innerHeight配合 JS 计算,把高度写成内联样式。这能解决一部分问题,但每次视口变化都要监听、重算、赋值,又慢又啰嗦。现在 CSS 终于给出了原生解法:把“视口”拆成小视口、大视口、动态视口三套单位。
1.2 svh、lvh、dvh:三组单位分别代表什么
先记一个关系,后面才不会混:
- 小视口(small viewport):浏览器工具栏完全展开、可用区域最小的时候。
- 大视口(large viewport):浏览器工具栏完全收起、可用区域最大的时候。
- 动态视口(dynamic viewport):当前真实可见的视口,会随工具栏状态实时变化。
对应到单位就是:
| 单位 | 含义 | 典型使用场景 |
|---|---|---|
svh | 小视口高度的 1% | 需要保证内容不被工具栏遮挡,比如底部按钮、弹层 |
lvh | 大视口高度的 1% | 需要稳定最大高度时,但实际用得不多 |
dvh | 动态视口高度的 1% | 希望高度跟随当前真实可视区域,比如全屏页面、滚动容器 |
同样的规则也适用于svw、lvw、dvw,只是单位从高度换成了宽度。宽度方向的动态变化在手机上不明显,所以最值得先替换的是高度。
1.3 移动端全屏布局的落地写法:先回退,再增强
新单位虽好,但你不能假设所有用户浏览器都认识。更稳妥的写法是:先写老单位,再写新单位,让支持的浏览器用新值覆盖旧值。
.hero { height: 100vh; /* 兜底:不支持新单位时用 */ height: 100svh; /* 小视口兜底 */ height: 100dvh; /* 现代浏览器优先使用 */ }CSS 的层叠规则在这里很重要:后面的声明会覆盖前面的声明。旧浏览器不认识svh、dvh,会直接忽略,继续使用100vh;新浏览器则按顺序解析,最后采用100dvh。
如果你希望逻辑更明确,也可以配合@supports:
@supports (height: 100dvh) { .hero { height: 100dvh; } }注意:声明顺序不能颠倒了。如果先把
100dvh写在最前面,再把100vh写在后面,现代浏览器最后会采用100vh,新单位等于白写。
实际项目里,我不建议把所有100vh都无脑替换成100dvh。有些场景要分清楚:底部操作栏为了避免被工具栏遮挡,更适合100svh;全屏轮播图需要跟随当前视口,用100dvh;如果只是普通页面背景,旧100vh也未必不能忍。按场景选,而不是按“新旧”选。
1.4 vi、vb 和 svmin/lvmax 这些单位,暂时不用记
视口单位里还有两个逻辑方向单位:vi和vb。vi是视口内联方向尺寸的 1%,vb是视口块方向尺寸的 1%。它们和 CSS 逻辑属性是一套思路,会随着writing-mode变化。对于大多数中文、英文横向排版的页面,vi近似宽度,vb近似高度,但暂时不是必需品。
至于svmin、lvmin、dvmax这一组,我认为属于“看一眼知道有这回事就行”的范畴。它们的语义太细,日常布局很难比min()、max()函数更直观。真遇到需求,再查规范也不迟。
2. 容器查询单位:让组件根据自己的父容器做响应式
2.1 媒体查询只管页面,管不到组件
媒体查询@media只能感知“视口”这个全局环境。页面宽度 1200px 时没问题,但同一个组件被放到侧边栏时宽度只有 300px,放到主内容区时宽度有 800px,媒体查询就束手无策了。
容器查询@container解决了“组件根据自己父容器宽度自适应”的问题。容器查询单位则把这个能力进一步量化,让元素不再只能靠视口单位或百分比,而是可以直接用“容器宽度的百分之几”这种单位来设置字号、间距、尺寸。
2.2 六个 cq 单位:cqw/cqh/cqi/cqb/cqmin/cqmax
容器查询单位一共有六个,都相对于最近的“查询容器”:
| 单位 | 含义 |
|---|---|
cqw | 查询容器宽度的 1% |
cqh | 查询容器高度的 1% |
cqi | 查询容器内联方向尺寸的 1%,横向排布下近似宽度 |
cqb | 查询容器块方向尺寸的 1%,横向排布下近似高度 |
cqmin | cqi和cqb中较小的那个 |
cqmax | cqi和cqb中较大的那个 |
这里最需要记住的是cqi。因为实际场景中,组件宽度响应的需求远大于高度。横向书写模式下,cqi就相当于容器的宽度百分比。
容器查询单位真正厉害的地方在于:它不是相对页面,也不是相对父元素百分比,而是相对“离自己最近的查询容器”。这让组件可以脱离页面全局宽度单独思考。
2.3 先建容器,单位才会生效
容器查询单位不能单独使用。你必须先把某个祖先元素定义成查询容器,它下面的后代才能使用cqw、cqi这些单位。
最简配置:
.card { container-type: inline-size; }container-type: inline-size的意思是:让该元素成为查询容器,并以它的内联方向尺寸作为查询基准。它不会强制容器必须有高度,所以是默认推荐值。如果写container-type: size,容器还需要显式设置高度,否则高度会塌陷,日常使用容易踩坑。
2.4 一个卡片组件示例:字号和内边距都跟着容器走
假设你有一个媒体卡片,可能被放在侧边栏,也可能放在主内容区。以前你需要通过媒体查询或者.sidebar .card这种嵌套选择器来覆盖。现在可以直接让卡片内部使用容器查询单位:
.media-card { container-type: inline-size; } .media-card__title { font-size: clamp(1.1rem, 4cqi, 2.4rem); } .media-card__body { padding: 2cqi; }当卡片宽度为 400px 时,4cqi就是 16px;当卡片宽度为 800px 时,4cqi就是 32px。clamp()又给字号设置了上下限,避免过小或过大。
这种写法让组件本身具备了“容器感知”能力,不再依赖页面级断点。
2.5 容器单位最容易踩的三个坑
第一个坑:忘了设置container-type。没有查询容器,cqi、cqw就直接失效。查问题时先看最近的祖先元素有没有加这个属性。
第二个坑:容器自身用容器查询单位。查询容器只能作为基准,它不能拿自己作为参照。如果你想给容器自身设置宽度,要用百分比、视口单位或者普通长度单位。
第三个坑:容器高度由内容撑起来时,不要用高度方向的cqh或cqb。高度取决于内容,内容又依赖单位,容易形成循环依赖。因此绝大多数场景优先用cqi。
注意:容器查询单位只对查询容器的后代生效。如果你写了一个组件,但页面里没有任何带
container-type的祖先,它不会自动退化成视口单位,结果就是声明无效。
3. 字体类新单位:lh、rlh、cap、ic 不是摆设,但也别全用
3.1 lh 和 rlh:把间距与行高绑定
lh单位是“当前元素行高”的百分比,1lh等于当前元素计算出来的行高。rlh则是根元素行高的百分比。
这个单位最大的价值,是让垂直间距和文字排版节奏保持一致。以前你为了“段落之间空一行”,通常写margin-bottom: 24px,但字号和行高一变,24px 的视觉比例就乱了。如果用1lh,间距会始终等于一行的高度:
.article p { line-height: 1.7; margin-bottom: 1lh; }这样无论字号怎么调整,段落间距始终是“一行”,排版比例不会崩。rlh更多用于全站基础组件,可以把全局间距统一锚定在根元素的行高上。
需要注意:不要把lh用在line-height属性本身上,这会产生循环定义。它更适合margin、padding、top、bottom这类长度属性。
3.2 cap:让按钮高度跟大写字母对齐
cap单位相对的是当前字体的大写字母高度,也就是从基线到大写字母顶部的距离。这个值比em更能反映“文字实际看起来多高”。
常见痛点:按钮高度设成40px,视觉上总觉得文字偏上偏下。用cap参与高度计算,可以让按钮高度紧密贴合实际字符高度:
.button { height: calc(2em + 0.5cap); padding: 0.25cap 1em; }这不能解决所有对齐问题,但在制作精细按钮、图标与文字混排时,比纯靠em、固定像素要准。
3.3 ic:在中文排版里值得留意
ic单位是表意字符的推进宽度,简单理解就是“一个汉字大概占多宽”。在中文环境里,这是一个很实用的度量概念。
比如你想限制某段文本显示大约 20 个字符宽:
.chinese-excerpt { width: 20ic; }在常见中文字体下,1ic会落在 1em 附近,但会根据具体字体修正。比起直接写20em,ic在理论上更贴合“汉字字符宽度”的语义。目前它的浏览器支持还不算广泛,落地前需要先确认目标环境,并写好回退。
3.4 字体类单位的坑:字体加载、单位值不稳定
字体类单位有一个共同问题:它们依赖字体文件加载完成后的度量值。如果页面使用了 web font,在字体加载前后,lh、cap、ic对应的像素值可能会变化,导致布局出现轻微抖动。
缓解办法是:
- 字体显示策略用
font-display: swap时,提前想到布局偏移; - 对关键排版区域,先设置足够接近的回退值;
- 上线前在真实网络环境里检查,而不是只看本地。
这类单位适合“锦上添花”,不建议在一开始就把所有间距都迁移到lh上。先在一个模块试,看效果再推广。
4. 几十个单位里,真正值得进项目的就这几类
4.1 四个判断维度:兼容性、可回退、收益、复杂度
面对一堆新单位,做技术选型不需要把每个都研究透。我会用四个问题来判断:
- 目标浏览器支持吗?
- 不支持时有无简单回退?
- 用完之后,代码是更简单还是更复杂?
- 会不会引入布局循环、字体抖动等副作用?
基于这四个问题,我给出现阶段的个人判断:
svh、dvh值得先大规模替换100vh;cqi、cqw值得在组件化项目里逐步引入;cqh、cqb要谨慎,确认容器高度是明确值再用;lh、rlh适合排版类模块,先小范围验证;cap、ic属于特定场景,按需使用;- 其他零散单位,暂时观望。
4.2 一张速查表:推荐度、典型场景、回退方案
下面这张表可以直接复制到项目文档里,作为团队参考:
| 单位类型 | 推荐度 | 典型场景 | 回退方案 |
|---|---|---|---|
dvh/svh | 高 | 移动端全屏、底部操作栏、弹层 | 先写100vh,再写新单位 |
cqi/cqw | 中高 | 卡片内字号、间距、组件级响应式 | 固定rem值或媒体查询 |
cqh/cqb | 中 | 容器高度明确时的内部布局 | 百分比或固定高度 |
cqmin/cqmax | 中低 | 特殊比例控制 | 不推荐优先使用 |
lh/rlh | 中 | 垂直节奏、段落间距 | rem倍数 |
cap | 中低 | 按钮高度、图标文字对齐 | em或固定像素 |
ic | 中低 | 中文字符宽度控制、古籍/中文版式 | em或固定宽度 |
vi/vb | 低 | 多书写模式场景 | 物理单位vw/vh |
注意,上表是“现阶段通用实践”下的经验排序,不是规范要求。你的项目如果只跑在最新的 Chromium 内核上,推荐度可以整体上调;如果还要兼容旧版本 WebView,就要保守得多。
4.3 clamp + cqi:组件级流式字号的最好组合
这是目前我认为最值得抄的一段实践:
.card { container-type: inline-size; } .card-title { font-size: clamp(1.25rem, 4cqi + 0.5rem, 2.75rem); }它的价值在于:字号不再只依赖页面视口,而是跟着最近的组件容器走;同时用clamp()限制了最小值和最大值,避免容器极窄或极宽时字体失控。
如果是全站统一字号,还可以抽成 CSS 变量:
:root { --title-size: clamp(1.25rem, 4cqi + 0.5rem, 2.75rem); } .card-title { font-size: var(--title-size); }不过要注意:CSS 变量是惰性计算的,cqi只在真正用到变量、且元素有查询容器的场景下才生效。别把--title-size放在根元素,然后期望全站所有标题都基于各自容器算,这个需要分别在不同组件里重新声明。
4.4 这些场景先别引入:旧项目、低版本 WebView、像素级还原
不是所有项目都适合追新单位。
- 旧项目:全局已有大量固定像素和断点,引入新单位会造成两套单位混用,维护成本反而增加。建议新页面、新模块先试。
- 低版本 WebView:很多 App 内嵌 WebView 的浏览器内核升级很慢,
dvh、cqi都可能不被识别。落地前必须确认最低支持版本。 - 像素级还原:如果设计稿要求精确到像素,容器查询单位会让设计验收变难。不是不能用,而是你要先说服团队成员接受“动态字号”这件事。
4.5 长期价值:从“页面级响应式”进入“组件级响应式”
这些新单位真正改变的事情,不是“多了几个单位”,而是把响应式的衡量基准从“页面视口”扩展到了“组件容器”。
过去做一套组件,要在页面级考虑它在不同宽度下怎么变;现在组件可以基于自身容器响应,复用性大幅提高。这就是我认为动态视口单位和容器查询单位最核心的长期价值:让组件成为一个真正自洽的单元。
5. 落地排查:新单位不生效时,按这个顺序找问题
5.1 先确认现象和输入
如果新单位写了但没效果,先别急着怀疑浏览器。第一步看现象:
- 是完全没有生效,还是部分浏览器不生效?
- 是字号没变,还是高度错乱?
- 是始终使用兜底值,还是布局抖动?
再看写法。CSS 单位名是大小写敏感的,规范的视口单位、容器查询单位都是小写。写100DVH、4CQI在大部分浏览器里不会被识别,整条声明会失效。还要检查calc()里的空格:calc(100dvh - 80px)中间的减号两侧必须有空格,否则声明非法。
5.2 再查浏览器环境和支持情况
新单位最大的敌人不是语法,而是浏览器版本。
建议用@supports做能力检测:
@supports (height: 100dvh) { .module { height: 100dvh; } }如果条件不成立,可以给一个明确回退。排查时也可以在开发者工具控制台里快速验证:
CSS.supports('height', '100dvh')返回true再排查别的问题;返回false说明当前浏览器根本不支持这个单位。
5.3 再查容器和布局上下文
这一步主要针对容器查询单位。很多“不生效”的案例,根本不是浏览器不支持,而是:
- 没有给祖先元素设置
container-type; - 设置的是
container-type: size,但容器没有明确高度,导致容器高度塌陷; - 使用
cqh但容器高度是由内容撑开的,产生了循环依赖; - 想把容器查询单位用在容器自身,但规范只允许用于后代。
解决办法是先建一个最小复现页面,只放一个容器和一个子元素,跑通后再套进真实组件。
5.4 最后查构建产物和工具链
如果你使用了 PostCSS、CSS Modules、cssnano 这类工具,还要检查构建产物里是否还保留着新单位。
正常情况下,压缩工具不会把合法单位删掉。但有些团队配置了比较激进的 PostCSS 插件,比如自动降级版本、自动加前缀的插件,可能会影响未知单位。排查时直接看最终打包出来的 CSS 文件,搜索dvh或cqi,确认它真的存在,并且没有被某个插件转换成别的值。
排查顺序总结成一张表:
| 排查层 | 常见问题 | 验证方式 |
|---|---|---|
| 写法 | 单位拼写错误、calc空格问题 | 查看源代码与 Network 返回的 CSS |
| 浏览器 | 版本过旧、WebView 不支持 | CSS.supports()检测 |
| 容器 | 没设置container-type、容器高度塌陷 | DevTools 查看容器尺寸与 Computed 值 |
| 构建 | PostCSS 插件错误处理新单位 | 查看打包后的 CSS 产物 |
5.5 一个最小实验模板
如果你想验证新单位在当前项目里能不能用,建议先做下面这个最小实验:
<div class="container"> <div class="item">Hello CSS</div> </div>.container { container-type: inline-size; width: 400px; border: 1px solid blue; } .item { font-size: 5cqi; padding: 2cqi; border: 1px solid red; }在浏览器里打开后,把.container的宽度改成 800px,观察.item的字号是否跟着变大。如果这个实验正常,说明浏览器支持、容器配置也正确。如果没变,说明问题出在环境或工具链上。
注意:先跑通一个最小案例,再做批量替换。一次引入太多新单位,出了问题很难定位是哪一层造成的。
CSS 新单位不是“学会所有新语法”,而是“知道哪些能真正改变工作流”。我的建议是,先从移动端100vh这个最痛的问题开始,把全屏模块替换成svh/dvh;再找一个卡片组件,试着用cqi控制内部字号。你不需要记住全部几十个单位,但这两组单位带来的体验变化,值得认真用起来。