过去几年 CSS 进化速度远超从前。容器查询、子网格、:has() 父选择器这三项特性在 2023 年陆续全浏览器落地后,到 2026 年已经成为生产环境里日常可用的工具。它们共同的特点是:把过去必须靠 JavaScript 才能实现的组件级响应逻辑,下沉到了 CSS 层,让样式更内聚、性能更好、维护成本更低。

这篇文章把我们团队过去半年在新组件库里实际落地的三个特性讲清楚,重点放在和旧方案的对比,以及哪些 JS 逻辑可以被它们彻底替换。

一、容器查询:基于组件容器而非视口

媒体查询 @media 是过去做响应式的唯一手段,它判断的是浏览器视口宽度。但实际业务里,组件最终展示的可用宽度并不等于视口宽度——同一个卡片可能被放在全宽主区域,也可能被塞进侧边栏或栅格里的某一列。视口没变,但组件可用空间变了,@media 在这种场景下完全无能为力。

容器查询 @container 解决的就是这个问题。它判断的是组件父容器的尺寸,组件长什么样取决于自己被分配到多大空间,而不是整个浏览器窗口多大。这才是真正的"组件级响应式"。

使用步骤:先声明容器,再写查询

/* 1. 在组件的外层容器上声明查询容器 */
.card-wrapper {
  container-type: inline-size; /* 只关心行内方向(宽度)尺寸 */
  container-name: card;        /* 可选:给容器起个名字 */
}

/* 2. 基于容器宽度写规则 */
.card {
  display: grid;
  gap: 12px;
}

@container card (min-width: 480px) {
  .card {
    grid-template-columns: 160px 1fr; /* 宽度够大时改成左图右文 */
  }
}

@container card (max-width: 280px) {
  .card {
    grid-template-columns: 1fr;       /* 很窄时纯纵向堆叠 */
  }
}

container-type: inline-size 是最常用的取值——它告诉浏览器只追踪容器的行内尺寸(在横排布局里就是宽度),开销可控。如果用 size 会同时追踪宽高,会禁用容器自身的尺寸自动计算,一般不推荐。

@container 与 @media 的本质区别

对比维度 @media 媒体查询 @container 容器查询
判断依据 浏览器视口(viewport) 组件的包含容器
响应粒度 页面级 / 整站级 组件级
组件复用 差(同一组件在不同布局下表现错乱) 好(跟随容器自适应,天然可复用)
是否需要 JS 介入 常配合 ResizeObserver 补救 纯 CSS,无需 JS

落到我们组件库里:过去一个 <UserCard> 要在窄边栏和宽列表里都好用,得通过 JS 读取父节点宽度再加 class 切换样式。换成容器查询后,组件内部 CSS 自己搞定,对外 API 零改动,前端 bundle 里还少了一段 ResizeObserver 的运行时代码。

二、:has() 父选择器:CSS 终于能"往后看"

历史上 CSS 选择器只能"从父往子、从前往后"匹配,无法根据子元素或后续兄弟节点的状态反向设置祖先样式。于是无数"父级根据子元素状态变色"的需求被丢给了 JS:classList.toggleaddEventListener 一通操作。:has() 这个被戏称为"父选择器"的伪类,把这部分能力还给了 CSS。

下面是我们在实际项目里落地的 5 个真实案例。

案例 1:表单校验高亮

当输入框关联的错误提示出现时,自动把整个表单项容器标红,无需 JS 操作状态。

.form-item:has(.error-msg:not(:empty)) {
  border-color: #e5484d;
  background: #fff5f5;
}
.form-item:has(input:invalid) {
  border-color: #e5484d;
}

案例 2:卡片悬停联动

鼠标悬停在卡片任意位置时,标题和图标同时变色——过去要给每个子元素单独写 :hover,现在从父级一处控制。

.card:has(.card-title:hover) .card-icon {
  color: #6d5efc;
}
/* 或反过来:悬停标题时整张卡片轻微抬升 */
.card:has(.card-title:hover) {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(109, 94, 252, 0.15);
}

案例 3:列表选中态

列表项被选中时给整个 <li> 加背景,过去要在 JS 里 toggle 一个 class,现在纯 CSS。

li:has(> input[type="checkbox"]:checked) {
  background: #f3f1ff;
  border-left: 3px solid #6d5efc;
}

案例 4:手风琴菜单

利用 :has() 配合 details 元素的 [open] 属性,可以在不写一行 JS 的情况下让手风琴展开时改变图标方向和整体高度。

details:has(> summary:not([data-closed]))[open] .chevron {
  transform: rotate(180deg);
  transition: transform 0.2s;
}
.group:has(details[open]) {
  background: #faf9ff;
}

案例 5:条件显示

某个区块只要内部存在"必填未填"的输入项,就让顶部提示条显示出来——典型的"基于内容存在性"的条件渲染。

.notice-bar { display: none; }
.notice-bar:has(+ form input:required:invalid) {
  display: flex; /* form 里有必填项未通过校验时显示提示条 */
}

这五个案例的共同点是:原来都需要监听事件 + 改 class(或维护 state),现在全部下沉到 CSS。代码量减少的同时,还省掉了重渲染和 reflow 的开销——样式变更由浏览器自己算,性能天然更好。

三、subgrid 子网格:让嵌套对齐不再是噩梦

CSS Grid 解决了二维布局,但有个长期痛点:嵌套的子容器一旦自己变成 grid,就独立了一套行列线,无法跟外层父网格对齐。最典型的场景是表格化数据卡片——外层卡片是网格,内层每张卡片的"标题/数值/单位"想跟左右邻居严格对齐,传统做法只能写死宽度或用 JS 测量。

subgrid 让子元素的 grid 轨道继承父网格的轨道定义,从而实现跨层级的精确对齐。

.metrics {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr; /* 三列等宽 */
  gap: 16px;
}

.metric-card {
  display: grid;
  grid-template-rows: subgrid;   /* 行方向继承父网格行线 */
  grid-row: span 3;              /* 占父网格 3 行 */
}

.metric-card .title,
.metric-card .value,
.metric-card .delta {
  /* 因为继承了父网格的行线,三行内容跨卡片天然对齐 */
}

对于更复杂的多列对齐场景,grid-template-columns: subgrid 同理:所有卡片的"标题列、数值列、单位列"共享父网格列线,无论内容长短都对得整整齐齐。这是过去即使加了 JS 也很难做稳的事——浏览器一缩放、字体一换,固定宽度方案立刻露馅,而 subgrid 永远跟随实际轨道。

四、这些特性如何替代过去的 JS 逻辑

把上面三块能力放在一起看,过去大量"为了响应式 / 联动 / 对齐"而存在的 JS 代码都可以删掉。我们新组件库 v3 重构时清理掉的部分逻辑:

过去的 JS 实现 现在的 CSS 方案 收益
ResizeObserver 监听容器宽度切 class @container 查询 无运行时监听,省内存
onChange / onClick 改父级状态 class :has() 选择器 无 reflow 触发,状态变更由浏览器算
useEffect 测量子节点高度做对齐 subgrid 子网格 无布局抖动(CLS 降低)
matchMedia 监听断点切换分支渲染 @container + CSS 变量 样式与逻辑解耦

一个值得强调的收益是首次渲染稳定性。JS 方案通常要等首帧绘制后才能测量并修正布局,造成肉眼可见的"跳动"——这正是 CLS(累计布局偏移)恶化的来源。CSS 原生方案在首帧就一次到位,从根上避免了这种抖动。

五、浏览器兼容性

这三项特性截至 2026 年中已全部跨主流浏览器稳定支持,下面的数据来自 Can I Use 与我们内部统计的真实覆盖率(2026 年 5 月采集)。

特性 Chrome Edge Firefox Safari 全球覆盖率
@container 容器查询 105+ 105+ 110+ 16+ 97.2%
:has() 选择器 105+ 105+ 121+ 15.4+ 96.8%
subgrid 子网格 117+ 117+ 71+ 16+ 95.5%

对我们业务覆盖的用户群(中国国内为主)来说,覆盖率会更高。团队决策是:从 v3 起这些特性直接进入主链路,不再为旧浏览器做降级 polyfill——对 95% 以上的现代浏览器来说,这意味着更小的 CSS 体积和更稳的渲染;对剩余极少数旧环境,我们会用 @supports 做条件兜底而非全量回退。

@supports not (container-type: inline-size) {
  /* 旧环境兜底:用固定的媒体查询提供基础布局 */
  .card { grid-template-columns: 1fr; }
}

六、小结

容器查询把响应式的判断基准从"视口"下沉到"组件容器",:has() 把样式对 DOM 状态的反向依赖能力还给 CSS,subgrid 把跨层级的轨道对齐变成一行声明。这三项加起来,覆盖了过去组件库里相当一部分靠 JS 维护的布局逻辑。

我们的实践结论很直接:能用 CSS 解决的布局,就不要进 JS。样式回到样式层、逻辑回到逻辑层,组件代码会更短、更快、更稳。2026 年还把这些新特性当"未来"的话,那就真的落后了——它们已经是今天该用的方案。