以前组件换肤要让父级配媒体查询,今天 CSS 自己会读自己的状态了——@container style() 把响应式从视口手里彻底抢回来了

我管组件的”变体”这件事,最早是用 CSS class 堆出来的。不同宽度配不同的 class,父级容器决定子组件长什么样。这个思路用了五六年,每次换设计稿都要改两处:父级媒体查询 + 子组件 class。

后来有了容器查询,组件终于不用知道自己在哪个父级容器里了——父级配 container-type: inline-size,子组件自己用 @container (min-width: ...) 查询尺寸。但这个还是有局限:只能查尺寸,不能查”状态”。

今天 CSS 终于把这个口堵上了——@container style() 允许组件直接查询自己的自定义属性值,组合 @property 注册类型后,连数值范围比较都能做。

@property 先把属性变成”可查的”

普通的 CSS 变量 var(--theme) 可以用,但无法被容器查询识别。需要先通过 @property 注册,告诉浏览器这个属性的类型:

@property --variant {
  syntax: "<custom-ident>";
  inherits: false;
  initial-value: "default";
}

@property --level {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

注册后的属性,会在 container-type: normal 的容器内建立”样式查询容器”上下文。注意:container-type: normal 不会建立尺寸查询容器,但可以建立样式查询容器。

@container style() 直接查自己

有了注册的 @property,组件可以直接查自己的状态,不需要父级参与:

.card {
  container-type: normal;
  container-name: card;
  /* 组件默认状态 */
  background: #f5f5f5;
  padding: 1rem;
}

@container card style(--variant: featured) {
  .card {
    background: linear-gradient(135deg, #667eea, #764ba2);
    padding: 2rem;
    color: white;
  }
}

@container card style(--variant: compact) {
  .card {
    padding: 0.5rem;
  }
}

配合数值范围查询,还能做等级制样式响应:

@container card style(--level > 3) {
  .card {
    border: 2px solid gold;
    box-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
  }
}

@container card style(--level > 7) {
  .card {
    border: 2px solid gold;
    box-shadow: 0 0 30px rgba(255, 215, 0, 0.5);
    animation: pulse 2s infinite;
  }
}

这和以前有什么本质区别

以前响应式是”外部驱动”的:视口宽度或父级宽度变了,通知子组件重新渲染样式。这个通知链路要靠媒体查询或 JS 手动切换 class。

@container style() 把这个链路彻底倒过来了——组件自己定义状态变量,自己查询这个变量,状态变样式自动变。没有外部通知,没有 class 切换,没有 JS 什么事。

来看一个完整的实战例子,把变体和尺寸查询组合起来:

@property --card-variant {
  syntax: "<custom-ident>";
  inherits: false;
  initial-value: "list";
}

@property --card-level {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

/* 列表视图:紧凑横向 */
@container card style(--card-variant: list) {
  .card { flex-direction: row; gap: 1rem; }
  .card-image { width: 80px; height: 80px; }
}

/* 卡片视图:垂直堆叠 */
@container card style(--card-variant: card) {
  .card { flex-direction: column; }
  .card-image { width: 100%; aspect-ratio: 16/9; }
}

/* 等级决定边框粗细 */
@container card style(--card-level > 5) {
  .card { border-width: 2px; }
}
@container card style(--card-level > 8) {
  .card { border-width: 3px; border-color: gold; }
}

HTML 里只需要给容器配上 data 属性,CSS 自己会驱动整个变体系统:

<div class="card-wrapper" style="--card-variant: featured; --card-level: 7;">
  <div class="card">...</div>
</div>

降级:没有 @property 怎么办

如果不用 @property 注册,直接在普通 CSS 变量上用 style(),浏览器会跳过这个查询,不报错也不生效。所以需要做两层降级:

.card {
  container-type: normal;
  /* 未注册时的默认值 */
  background: #f5f5f5;
}

/* 已注册属性的增强样式 */
@property --card-variant {
  syntax: "<custom-ident>";
  inherits: false;
  initial-value: "default";
}

@container card style(--card-variant: featured) {
  .card { background: linear-gradient(135deg, #667eea, #764ba2); }
}

降级后的表现是组件退回到默认值,但不会坏。这个模式可以安全地逐步增强。

浏览器支持情况

@container style() 属于 Container Queries 规范,Chrome 106+ / Firefox 110+ / Safari 16+ 均已支持。@property 的支持范围更大,Chrome 85+ / Firefox 128+ / Safari 16.4+。

如果你的项目需要兼容更老的浏览器,可以用 @supports 做特性检测,或者在不支持的环境下回退到 JS 切换 class 的方案——但新项目完全可以默认开这个能力。

下一步怎么做

下次给组件设计变体系统时,先问自己一个问题:这个变体,真的需要父级来通知吗?

如果变体是由组件自身属性决定的(比如卡片等级、用户角色、数据状态),把 --variant 注册成 @property,组件自己 @container style() 查自己。如果变体是由容器尺寸决定的,继续用 @container (min-width: ...) 尺寸查询。

这两件事以前混在一起写,现在彻底拆开了。组件终于可以「自己管自己的状态」了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 860 阅读