现代 CSS:容器查询、:has()、@layer
媒体查询看的是视口。一张卡片放进窄侧栏时,视口可能仍是桌面宽度,卡片自己却已经挤变形了。容器查询、:has() 和 @layer 解决的是三件不同的事,不要当成一套「新 CSS 语法」一起上。
1. 容器查询
媒体查询问的是「视口有多宽」,容器查询问的是「我这个组件被塞进了多宽的地方」。同一个商品卡片放在 320px 的侧栏和 900px 的主区里,视口宽度完全一样,媒体查询给不出答案,容器查询可以。写法上要先给父元素声明 container-type: inline-size(只关心内联方向尺寸时用这个,代价最小),之后子元素才能用 @container (min-width: 480px) 写分支。
.card-shell {
container-type: inline-size;
container-name: card;
}
.card-body {
font-size: 14px;
}
/* 查询最近的名为 card 的容器,而非视口 */
@container card (min-width: 480px) {
.card-body {
font-size: 16px;
display: grid;
grid-template-columns: 1fr 2fr;
}
}
为什么元素不能查询自身:如果允许一个元素既当容器又被自己查询,它的尺寸变化会反过来影响自身查询条件,形成循环依赖,浏览器无法解析。所以容器查询查的是「最近的祖先容器上下文」,这也是为什么要先在父级声明 container-type。
2. 风格查询
容器查询默认按尺寸分支,但有时我们想按「容器暴露的某个自定义属性」分支,而不是按它的宽高。这就是风格查询(style query),它让主题、密度这类样式不必依赖 class 层层传递到每一个子节点:
.theme-scope {
container-type: inline-size;
--density: compact;
}
@container style(--density: compact) {
.panel { padding: 8px; }
}
@container style(--density: spacious) {
.panel { padding: 20px; }
}
父容器声明一次 --density,所有后代按风格查询自行取用,组件内部不再需要知道自己处于哪种密度档位。
3. :has() 父级选择器
:has() 让 CSS 第一次能根据「子孙的状态」反向上样式。过去「父元素根据子元素是否存在、是否聚焦」只能靠 JS 切 class,现在纯 CSS 就能表达。
/* 有图和无图的卡片用不同版式 */
.card:has(.card-cover) {
grid-template-rows: auto 1fr;
}
.card:not(:has(.card-cover)) {
padding-block: 16px;
}
/* 表单:只要组内出现校验失败项,整组标红边 */
.field-group:has(:user-invalid) {
border-color: #dc2626;
}
/* 标签后面的输入框必填时加星号 */
label:has(+ input:required)::after {
content: " *";
color: #dc2626;
}
它本质是一个「关系型伪类」,不是选择器性能杀手。真正的代价来自选择器复杂度:写在 body 上的 body:has(:hover) 会在每次悬停时触发整文档重算,要控住范围、尽量用子代选择器。还要注意 :has() 不能嵌套在另一个 :has() 的参数里(规范明确禁止),写了整条规则会被丢弃且无报错——这是它最容易让人踩的静默失败。
4. @layer 层叠层
层叠层解决的是一个很具体的老问题:为了盖住第三方库的样式,只能把选择器写长或者上 !important,时间一长谁也改不动。有了 @layer reset, base, components, utilities,优先级由层的声明顺序决定,与选择器权重无关——写在 utilities 层的单类名,天然压过 components 层里再长的选择器。
/* 顺序只在第一次声明时确定 */
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; box-sizing: border-box; }
}
@layer components {
.btn-primary { background: #2563eb; }
}
@layer utilities {
.bg-red { background: #dc2626; }
}
/* .bg-red 一定压过 .btn-primary,与选择器长短无关 */
层的顺序只在第一次声明时确定,后面重复写同名层不会重新排序,所以声明语句要放在样式表最前面一次性写完。!important 依旧会反转层内与层间的顺序——层的存在不消除 important 的特殊性,遇到需要覆盖 utilities 层的情况仍然要靠 important 或调整层顺序。
5. 同期补齐的另外几项
这几项截至 2026 年 10 月也已进入 Baseline 广泛可用,可以放心用:
@property:给自定义属性注册类型与初值,让渐变角度、颜色这类原本无法插值的值能平滑动画。color-mix():在 oklch / oklab 空间里原生混色,hover 态、深色表面色调不再靠手算色板。oklch()/oklab():感知均匀的色彩空间,调亮度不偏色,比 hsl 更适合生成可访问调色板。subgrid:子网格对齐到父网格的线,复杂表单与卡片栅格的利器。:focus-visible:只在键盘聚焦时显示焦点环,鼠标点击不抢戏。- 原生 CSS 嵌套:新项目基本不必再为嵌套单独跑 Sass。
/* @property 让角度可动画 */
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.spinner {
background: conic-gradient(from var(--angle), #2563eb, #9333ea);
transition: --angle 1s linear;
}
用之前都确认一下目标运行环境,别为了「新」而新。
容器查询能替代媒体查询吗?不能。两者问的对象不同——媒体查询看视口,容器查询看父容器,实际项目里是叠加着用的,页面级布局仍然靠媒体查询。
容器也不能查询自身尺寸,它查的是最近的祖先容器上下文。让元素依赖自己的尺寸来定自己的样式,会形成循环依赖,规范直接把这个口子堵上了。
:has() 常被当成「父选择器」而没人敢用,其实它是关系型伪类,浏览器对匹配做了优化。真正要控制的是选择器本身的复杂度,嵌套过深才有代价。至于 @layer,它并不比 !important 更高——important 依旧会反转层内与层间的顺序,层只是多了一层可管理的优先级,不等于取消了 important 的特殊性。
这三件事的共同点是——把原本要靠 JS 或靠约定的判断交还给 CSS,用之前先确认支持度,用之后别再写两套。
6. 侧栏里的卡片不该看视口
媒体查询看的是窗口。窗口是 1440 像素,侧栏只有 280。卡片若按 min-width: 900px 排成横的,在侧栏里会挤出容器。
把卡片的容器标成查询容器,查询条件写卡片自己的宽度。侧栏里不足 320 就改成上下排,主栏里超过 320 再横排。同一张卡片放进对话框、放进侧栏、放进主内容,不用为每个位置写一套类名。
:has() 用来根据子元素改变父级,例如卡片里有图时才加上下边距。不要写在 body 上监听任意悬停,那会让整页在鼠标移动时反复计算。@layer 把重置、组件、工具类分成层,后写的工具类不会因为选择器更短而压不住组件样式。三件事解决的问题不同,不要为了「用上新语法」在一个组件里各写一遍。
容器查询的语法细节以 MDN:CSS Container Queries 为准,网上很多示例漏写了 container-type,照抄会完全没有效果。