CSS新单位实战:dvh、cqi如何解决移动端视口与组件响应式问题
2026/9/12 5:34:10 网站建设 项目流程

一不留神,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%希望高度跟随当前真实可视区域,比如全屏页面、滚动容器

同样的规则也适用于svwlvwdvw,只是单位从高度换成了宽度。宽度方向的动态变化在手机上不明显,所以最值得先替换的是高度。

1.3 移动端全屏布局的落地写法:先回退,再增强

新单位虽好,但你不能假设所有用户浏览器都认识。更稳妥的写法是:先写老单位,再写新单位,让支持的浏览器用新值覆盖旧值。

.hero { height: 100vh; /* 兜底:不支持新单位时用 */ height: 100svh; /* 小视口兜底 */ height: 100dvh; /* 现代浏览器优先使用 */ }

CSS 的层叠规则在这里很重要:后面的声明会覆盖前面的声明。旧浏览器不认识svhdvh,会直接忽略,继续使用100vh;新浏览器则按顺序解析,最后采用100dvh

如果你希望逻辑更明确,也可以配合@supports

@supports (height: 100dvh) { .hero { height: 100dvh; } }

注意:声明顺序不能颠倒了。如果先把100dvh写在最前面,再把100vh写在后面,现代浏览器最后会采用100vh,新单位等于白写。

实际项目里,我不建议把所有100vh都无脑替换成100dvh。有些场景要分清楚:底部操作栏为了避免被工具栏遮挡,更适合100svh;全屏轮播图需要跟随当前视口,用100dvh;如果只是普通页面背景,旧100vh也未必不能忍。按场景选,而不是按“新旧”选。

1.4 vi、vb 和 svmin/lvmax 这些单位,暂时不用记

视口单位里还有两个逻辑方向单位:vivbvi是视口内联方向尺寸的 1%,vb是视口块方向尺寸的 1%。它们和 CSS 逻辑属性是一套思路,会随着writing-mode变化。对于大多数中文、英文横向排版的页面,vi近似宽度,vb近似高度,但暂时不是必需品。

至于svminlvmindvmax这一组,我认为属于“看一眼知道有这回事就行”的范畴。它们的语义太细,日常布局很难比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%,横向排布下近似高度
cqmincqicqb中较小的那个
cqmaxcqicqb中较大的那个

这里最需要记住的是cqi。因为实际场景中,组件宽度响应的需求远大于高度。横向书写模式下,cqi就相当于容器的宽度百分比。

容器查询单位真正厉害的地方在于:它不是相对页面,也不是相对父元素百分比,而是相对“离自己最近的查询容器”。这让组件可以脱离页面全局宽度单独思考。

2.3 先建容器,单位才会生效

容器查询单位不能单独使用。你必须先把某个祖先元素定义成查询容器,它下面的后代才能使用cqwcqi这些单位。

最简配置:

.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。没有查询容器,cqicqw就直接失效。查问题时先看最近的祖先元素有没有加这个属性。

第二个坑:容器自身用容器查询单位。查询容器只能作为基准,它不能拿自己作为参照。如果你想给容器自身设置宽度,要用百分比、视口单位或者普通长度单位。

第三个坑:容器高度由内容撑起来时,不要用高度方向的cqhcqb。高度取决于内容,内容又依赖单位,容易形成循环依赖。因此绝大多数场景优先用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属性本身上,这会产生循环定义。它更适合marginpaddingtopbottom这类长度属性。

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 附近,但会根据具体字体修正。比起直接写20emic在理论上更贴合“汉字字符宽度”的语义。目前它的浏览器支持还不算广泛,落地前需要先确认目标环境,并写好回退。

3.4 字体类单位的坑:字体加载、单位值不稳定

字体类单位有一个共同问题:它们依赖字体文件加载完成后的度量值。如果页面使用了 web font,在字体加载前后,lhcapic对应的像素值可能会变化,导致布局出现轻微抖动。

缓解办法是:

  • 字体显示策略用font-display: swap时,提前想到布局偏移;
  • 对关键排版区域,先设置足够接近的回退值;
  • 上线前在真实网络环境里检查,而不是只看本地。

这类单位适合“锦上添花”,不建议在一开始就把所有间距都迁移到lh上。先在一个模块试,看效果再推广。

4. 几十个单位里,真正值得进项目的就这几类

4.1 四个判断维度:兼容性、可回退、收益、复杂度

面对一堆新单位,做技术选型不需要把每个都研究透。我会用四个问题来判断:

  1. 目标浏览器支持吗?
  2. 不支持时有无简单回退?
  3. 用完之后,代码是更简单还是更复杂?
  4. 会不会引入布局循环、字体抖动等副作用?

基于这四个问题,我给出现阶段的个人判断:

  • svhdvh值得先大规模替换100vh
  • cqicqw值得在组件化项目里逐步引入;
  • cqhcqb要谨慎,确认容器高度是明确值再用;
  • lhrlh适合排版类模块,先小范围验证;
  • capic属于特定场景,按需使用;
  • 其他零散单位,暂时观望。

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 的浏览器内核升级很慢,dvhcqi都可能不被识别。落地前必须确认最低支持版本。
  • 像素级还原:如果设计稿要求精确到像素,容器查询单位会让设计验收变难。不是不能用,而是你要先说服团队成员接受“动态字号”这件事。

4.5 长期价值:从“页面级响应式”进入“组件级响应式”

这些新单位真正改变的事情,不是“多了几个单位”,而是把响应式的衡量基准从“页面视口”扩展到了“组件容器”。

过去做一套组件,要在页面级考虑它在不同宽度下怎么变;现在组件可以基于自身容器响应,复用性大幅提高。这就是我认为动态视口单位和容器查询单位最核心的长期价值:让组件成为一个真正自洽的单元

5. 落地排查:新单位不生效时,按这个顺序找问题

5.1 先确认现象和输入

如果新单位写了但没效果,先别急着怀疑浏览器。第一步看现象:

  • 是完全没有生效,还是部分浏览器不生效?
  • 是字号没变,还是高度错乱?
  • 是始终使用兜底值,还是布局抖动?

再看写法。CSS 单位名是大小写敏感的,规范的视口单位、容器查询单位都是小写。写100DVH4CQI在大部分浏览器里不会被识别,整条声明会失效。还要检查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 文件,搜索dvhcqi,确认它真的存在,并且没有被某个插件转换成别的值。

排查顺序总结成一张表:

排查层常见问题验证方式
写法单位拼写错误、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控制内部字号。你不需要记住全部几十个单位,但这两组单位带来的体验变化,值得认真用起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询