我要提问
ARTICLE / 008 · 前端开发

原创文章

资深开发者执笔的深度技术长文,从原理到工程落地,逐层拆解。

现代 CSS 容器查询与作用域实战

现代 CSS 容器查询与作用域实战

媒体查询(@media)服务了响应式 Web 十多年,但它有一个根本缺陷:它响应的是视口尺寸,而不是组件尺寸。一个放在侧边栏的卡片和放在主内容区的卡片,视口相同但可用空间天差地别,却要共用同一套断点。容器查询(@container)解决了这个错配,让组件真正"按自己的容器"响应。配合 @scope,现代 CSS 终于能写出与上下文解耦的可复用组件。

一、媒体查询的局限:为什么需要容器查询

假设有一个卡片组件,宽屏时横向布局(图左文右),窄屏时纵向布局(图上文下)。用 @media 写,断点基于视口:视口 > 768px 横向,否则纵向。但这个卡片可能出现在三处:全宽主区、半宽侧栏、弹窗里。视口相同,侧栏里的卡片宽度只有主区一半,却被迫用横向布局,挤成一团。

媒体查询的本质是"页面级响应",容器查询的本质是"组件级响应"。前者问"屏幕多大",后者问"我有多大"。可复用组件应该回答后者,因为它不知道自己会被放到多大的容器里。

二、@container 的基本用法

容器查询分两步:先把某个元素声明为"查询容器",再用 @container 查询它的尺寸。声明容器用 container-type,最常用的是 inline-size(按行内尺寸响应,符合多数横向布局场景)。

/* 1. 声明容器:卡片的外层 */
.card-wrap {
  container-type: inline-size;
  container-name: card; /* 可选:命名容器 */
}

/* 2. 查询容器尺寸:按容器宽度切换布局 */
@container card (min-width: 480px) {
  .card {
    display: flex;       /* 宽容器:横向 */
    gap: 16px;
  }
  .card .thumb {
    width: 200px;
  }
}

/* 默认(窄容器):纵向 */
.card {
  display: block;
}

这样,同一个 .card 结构放进 600px 的主区就是横向,放进 300px 的侧栏就是纵向,完全由容器宽度决定,与视口无关。组件一次封装,处处自适应,这正是设计系统梦寐以求的特性。

三、容器查询单位:cqw / cqi

容器查询不只查询尺寸,还提供基于容器的单位。1cqw = 容器宽度的 1%,cqi 是 inline-size 的 1%。这让组件内部的字号、间距、圆角都能跟随容器缩放,而不依赖视口的 vw/vh。

.card-wrap { container-type: inline-size; }

.card {
  /* 字号随容器宽度缩放,容器越宽字越大 */
  font-size: clamp(14px, 3cqw, 22px);
  padding: 2cqw;
  border-radius: 1.5cqw;
}

配合 clamp() 设上下限,既能弹性缩放又不会失控。这种"组件内部单位"的思路,让一个组件在不同容器里保持视觉比例一致,是设计系统里非常实用的技巧。

四、@scope:样式作用域控制

另一个长期困扰 CSS 的问题是"样式作用域"。BEM、CSS Modules、Shadow DOM 都是不同时期给出的解。原生 @scope 规则让样式只在指定范围内生效,从语法层面提供了作用域能力,且比 Shadow DOM 轻量。

/* 只影响 .article 内的 h2,不影响其他区域 */
@scope (.article-content) {
  h2 {
    font-size: 22px;
    border-left: 4px solid #6B4FAE;
  }
  p { line-height: 2; }
}

/* 带下界:从 .article 到 .ad 之间生效,不渗入广告位 */
@scope (.article-content) to (.ad-slot) {
  a { color: #FF6FA5; }
}

4.1 @scope 与 Shadow DOM 的取舍

  • @scope:轻量、可组合、不隔离 DOM,适合普通组件库与内容样式。
  • Shadow DOM:强隔离(样式与 DOM 都不渗漏),适合第三方嵌入、需要完全封装的组件。
  • 多数业务组件用 @scope 足够,强隔离需求才上 Shadow DOM。

五、实战:一个真正可复用的卡片组件

把容器查询、容器单位、@scope 结合,写一个在任何容器里都自适应的卡片组件。

/* 组件根:声明容器 + 作用域 */
.ds-card {
  container-type: inline-size;
}

@scope (.ds-card) {
  .ds-card__inner {
    display: grid;
    gap: clamp(8px, 2cqw, 20px);
    padding: clamp(12px, 3cqw, 28px);
  }

  /* 窄容器:单列;宽容器:双列 */
  @container (min-width: 420px) {
    .ds-card__inner {
      grid-template-columns: 120px 1fr;
      align-items: center;
    }
  }

  .ds-card__title {
    font-size: clamp(15px, 3.2cqw, 20px);
  }
}
这个组件的精妙之处:无论你把它放进弹窗、侧栏、网格的哪一格,它都会根据所在容器的宽度自动切换单列/双列、缩放字号间距。组件内部零 JS、零媒体查询,纯 CSS 实现"环境自适应"。

六、浏览器兼容与渐进增强

@container 与 @scope 已在主流浏览器(Chrome、Edge、Firefox、Safari 较新版本)全面支持。对旧浏览器可做渐进增强:先用媒体查询兜底基础布局,再用 @supports (container-type: inline-size) 叠加容器查询增强。

  • @supports 检测容器查询支持,渐进增强。
  • 设计系统组件库优先采用 @container,让消费方无需关心断点。
  • 避免在容器元素上同时设 container-type: size 与百分比高度,会陷入"鸡生蛋"的高度循环。
  • container-type: size 需要显式给容器高度才生效,多数场景用 inline-size 即可。

结语

容器查询把响应式的粒度从"页面"下沉到"组件",是 CSS 设计哲学的一次修正。@scope 则补上了长期缺失的原生作用域能力。两者结合,让现代 CSS 第一次有能力写出真正与上下文解耦、可复用、自适配的组件。在 mrgr.cn 前端组的设计系统迁移中,引入 @container 后,组件断点逻辑从消费方移回了组件内部,业务侧的 CSS 体积下降了约 30%。欢迎在问答社区交流你的容器查询实践。

返回文章列表