这几个特性的可用程度差得很远:@scope(Chrome 118+)、Container Queries、:has() 和 text-wrap 可以按渐进增强铺开,scroll-driven animations 只适合做装饰性动效,masonry 还在语法分歧里,先别写进组件库。

按支持度分三档

特性 大致门槛 建议用法
Container Queries Chrome 105+、Safari 16+、Firefox 110+ 进基线,组件响应式的默认方案
:has() Chrome 105+、Safari 15.4+、Firefox 121+ 进基线,但限制选择器复杂度
@scope Chrome 118+、Safari 17.4+,Firefox 未默认开启 渐进增强,关键样式不能只写在作用域里
text-wrap: balance Chromium 114+ 起,其它引擎近期版本跟进 标题、卡片标题
text-wrap: pretty 以 Chromium 实现为主 正文段落,注意断行开销
scroll-driven animations Chromium 默认开启,Safari 与 Firefox 以官方文档为准 装饰性动效
masonry 无默认实现 只做预研

版本号只是门槛参考,真要写进兼容性矩阵以官方文档和 caniuse 为准。分档之后规则就三条:

  • 进基线:Container Queries、:has(),直接写进组件样式。
  • 渐进增强:@scope、text-wrap、scroll-driven animations,支持了更好,不支持页面也得能用。
  • 只做预研:masonry。

@scope 挡的是样式外溢

@scope (.card) to (.card__body) {
  img { border-radius: 8px; }
}

@scope (.card) to (.card__body) 是个甜甜圈作用域:.card 及其后代都在范围内,命中 .card__body 后停止向下。根元素本身不参与匹配,要选中它得写 :scope

省事的地方在权重。同一层里出现多个作用域时,作用域根离目标元素更近的那条规则赢,不用靠 !important 或者叠一长串类名去压。组件套组件时,内层 .title 自然盖住外层 .title

@scope (.card) { .title { font-size: 1rem; } }
@scope (.card) to (.card__body) { .title { font-size: .875rem; } }

两个坑。@scope 只约束选择器匹配范围,继承、自定义属性、层叠上下文一概不管,从外层继承进来的 color 照样生效。浏览器不认识 @scope 时整块规则被丢弃,所以先写普通选择器版本,再用 @scope 收敛,反过来写会直接丢样式。

它跟 Shadow DOM 的隔离不是一个量级,也没有 CSS Modules 那种编译期改名,选型时别把它当隔离方案用。

Container Queries 与容器单位

.card-host {
  container: card / inline-size;
}

@container card (inline-size >= 30rem) {
  .card { grid-template-columns: 8rem 1fr; }
}

container 简写是 <name> / <type>inline-size 是绝大多数场景该选的类型,只锁 inline 方向,块方向还能被内容撑开。

容器单位有 cqw、cqh、cqi、cqb、cqmin、cqmax,都相对最近的查询容器。cqi 是 inline 方向,横排文字里就是宽度,做卡片内边距、图标尺寸合适:padding: 4cqi。字号用 cqi 时要配 clamp(),不然窄容器里会小到看不清。

三个坑。container-type: inline-size 会带来 inline-size containment,元素宽度不再由内容撑开,放进 flex 行里做 shrink-to-fit 会塌成 0 宽,得给它明确的宽度来源(grid 轨道、flex: 1width: 100%)。这个坑我踩过,排查了半天以为是 flex 的问题。layout containment 还会让容器成为绝对定位和固定定位后代的包含块,原本相对视口定位的浮层会被关进卡片里。还有一条,容器查不了自己,@container 命中的是最近的祖先容器,别在容器元素自身的样式里写查询。

容器命名和查询分层

规范按这几条走:

  • container-name 全小写加短横线,前缀表达角色:layout- 给页面骨架,card-panel-list- 给组件。
  • 一个元素只当一种容器。挂了 container-type 就别再让它承担 grid 尺寸计算,两件事会互相打架。
  • 每个组件的阈值写在自己文件顶部,2 到 3 档,不跨组件复用别人的容器名。
  • 布局层用媒体查询,组件层用容器查询,两层不交叉。页面级断点变化由 layout- 容器往下传。
  • 要兼容旧内核就先写媒体查询版本,再在 @container 里覆盖,顺序不能反。
/* card.css */
/* 阈值:24rem 单列 / 40rem 双列,改动同步更新这行注释 */
.card-host { container: card / inline-size; }

@container card (inline-size >= 24rem) { /* 横向排列 */ }
@container card (inline-size >= 40rem) { /* 三栏 */ }

阈值只有 2 到 3 档是有原因的:容器查询的档位一多,组件在每个宽度下的样子都要单独验证,维护成本比媒体查询高得多,因为组件出现在哪个容器里由使用方决定。

:has() 的写法和失效代价

.field:has(input:invalid:not(:placeholder-shown)) .hint { color: #b3261e; }
.card:has(> img) { padding-top: 0; }
.list li:has(+ li) { border-bottom: 1px solid #e5e5e5; }

:has() 里是相对选择器,>+~ 都能用;不能嵌套 :has(),也不能把伪元素塞进去。

要留意的是失效成本。后代节点变化会沿祖先链重新匹配,Chrome 有失效集优化,但代价仍跟「可能匹配的祖先数量 × 选择器复杂度」挂钩。锚点尽量靠近变化点,别写 body:has(.modal-open) 这种从根往下找的,DOM 一大就是最贵的写法;别用 :has(*);能用 :focus-within 表达的交互状态优先用它,失效范围更小。

列表场景尤其小心。给几千行 li 都挂 :has(+ li) 画分隔线,每次插入删除都会触发一批重新匹配,换成 :not(:last-child) 便宜得多。

text-wrap 的边界

h2, .card__title { text-wrap: balance; max-inline-size: 28ch; }
p { text-wrap: pretty; }

balance 只对短块有效,Chromium 对参与平衡的行数有上限,超了按普通换行处理,标题超过四五行基本没效果。它还依赖块本身有宽度约束,块宽足够时压根没有断行可平衡,所以得配 max-inline-size 才看得到差别。中文标题字数少,平衡前后经常看不出区别,收益主要在英文标题和卡片标题上。

pretty 处理的是孤字和末行词数,断行计算比 balance 重,别给长列表的每一行都加。两个值不要在同一处纠结选哪个,按元素类型分开用。

scroll-driven animations 能用到什么程度

.progress {
  transform-origin: left;
  animation: grow linear;
  animation-timeline: scroll(root block);
  animation-range: 0% 60%;
}

@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

scroll() 绑定滚动容器的进度,view() 绑定元素进入视口的区间,animation-range 决定在哪一段生效。滚动进度条、进入视口淡入、吸顶栏收缩这几类动效可以直接替掉 JS 监听。

红利只在能合成的属性上。动 transformopacity 跑在合成线程,动 widthheightfilter 一样掉帧,而且比 JS 更难查,代码里看不到任何监听。需要按进度做非视觉逻辑时,用 JS 侧的 ScrollTimeline / ViewTimelinecurrentTime,别退回去监听 scroll 事件。

Chromium 默认开启,其它引擎的进度以官方文档为准,所以整套动效包在 @media (prefers-reduced-motion: no-preference) 里,不支持的浏览器看到的就是静态页面。

masonry 还在语法分歧里

两套提案在争:瀑布流做成 grid 的一个取值(grid-template-rows: masonry),还是做成独立的布局模式(display: masonry)。浏览器厂商站队不同,实验实现都有但默认关闭,最终语法没定,以 CSSWG 记录和各家文档为准。

现在要做瀑布流只有三条路。columns 最省代码,但排布是列优先,DOM 顺序和视觉顺序对不上,键盘 Tab 和屏幕阅读器会乱,也不适合「加载更多」按序插入。JS 库可控性最好,代价是布局在 JS 里算,SSR 首屏会跳一次。grid 手动算 grid-row: span n 能保住顺序,但行高要靠内容高度算,图片懒加载完成后还得重算一轮。

盯语法的话,别急着按某一套写封装,等 CSSWG 定下来再动。