容器查询
Tailwind CSS 容器查询:@container 声明参考系、@sm:/@md: 容器变体与媒体查询断点对照、命名容器消除嵌套歧义,组件级响应式实践
响应式设计的老工具是媒体查询:视口宽度小于某个值就换布局。但它有个盲区——组件不知道自己被放在多大的盒子里。同一张歌曲卡片,放在 280px 的侧栏要竖排,放在 900px 的主列表要横排,媒体查询只能”猜视口”,猜不对就出事。容器查询(Tailwind 4 已内置)把响应式的参考系从视口换成组件的父容器,组件从此自带响应式逻辑。
前置知识
- Tailwind 布局:Flex 与 Grid:卡片内部仍用 Flex/Grid 排布,容器查询只决定”何时切换”。
- Tailwind 响应式与暗色模式:md: 媒体查询变体是本篇 @md: 的对照组。
- Tailwind 工具类核心机制:变体语法一脉相承,@ 前缀是容器查询的标志。
学习目标
- 能说出媒体查询的盲区在哪里,解释容器查询补上了什么。
- 能用
@container标注容器,用@sm:系变体按容器宽度切换样式。 - 能为嵌套容器命名(
@container/sidebar)并用@md/sidebar:指定参考系。 - 能对比媒体查询与容器查询的适用层级,形成”页面用 md:、组件用 @md:“的分工直觉。
- 能把歌曲卡片做成”一份代码、三种位置通吃”的真响应式组件。
1. 媒体查询的盲区:组件不知道自己多大
先看盲区怎么形成。歌单页有两个位置放同一张歌曲卡片:右侧 280px 的”正在热播”侧栏、主区约 900px 的”本周新歌”列表。用媒体查询写”视口到 md 就横排”,结果桌面上侧栏那一张也跟着横排——侧栏只有 280px 宽,横排的封面加文字挤成一团。问题不在断点取值,而在参考系错了:md: 量的是视口,可卡片要适配的是父盒子。
容器查询把参考系换成”最近的容器祖先”:你先声明某个元素是容器,它的后代就能用容器变体问”容器多宽了”。语义从”屏幕到这个宽度了”变成”我的盒子到这个宽度了”。这一字之差,把响应式判断的归属权从页面移交给了组件——组件走到哪里,逻辑跟到哪里,与放置位置无关。
在容器查询成为标准能力之前,组件级的这类适配只能靠 JavaScript:用 ResizeObserver 监听卡片尺寸、手动切换类名,每个组件库都要自配一套逻辑。浏览器把这件事下沉到布局引擎后,响应式不再依赖脚本,样式在渲染管线里就能完成切换——这也是 Tailwind 4 敢把它直接内置进变体体系的原因:它已经是稳定、可依赖的 CSS 基础设施,不再是实验特性。
2. @container:给组件一个参考系
用法分两步。第一步,给包裹元素加 @container 类,它会被编译为 container-type: inline-size——浏览器开始追踪这个盒子的行内尺寸(宽度);第二步,在它的后代上用 @sm:、@md:、@lg: 等容器变体,断点参照的是容器宽度而非视口。注意容器变体有自己的刻度(@sm 是 24rem、@md 是 28rem,与媒体查询的 md: 48rem 不是一套数):
<!-- 歌曲卡片:容器查询让同一份代码在侧栏与主区都好看 -->
<div class="@container">
<article class="flex flex-col gap-3 @md:flex-row @md:items-center">
<img
src="/senbonzakura.jpg"
alt="千本樱封面"
class="w-full rounded-md @md:w-40"
/>
<div class="min-w-0">
<h3 class="text-base @lg:text-xl">千本樱</h3>
<p class="text-sm text-text-secondary">P主:黑うつP</p>
<!-- 窄容器里隐藏次要信息,宽到一定程度才显示 -->
<p class="hidden text-sm @md:block">专辑:《ALL THAT 千本桜》</p>
</div>
</article>
</div>
变体刻度之外的任意宽度用方括号:@min-[520px]:grid-cols-2、@max-md:hidden(容器窄于 @md 时隐藏)。与媒体查询一样,@max-* 处理”小于就”的语义,适合做渐进降级。刚把 @container 加上去时容易犯的第一个错是忘了加容器类——没有参考系,@md: 变体全部静默失效,页面看起来”这个类不存在”。
3. 命名容器与嵌套:谁说了算
容器可以嵌套,也可以命名。命名解决两个问题:变体默认参考最近的祖先容器,嵌套一深就可能找错对象;同一个组件同时需要参考两个不同容器的宽度。
<!-- 布局骨架:侧栏与主区各自是命名容器 -->
<div class="flex gap-6">
<aside class="@container/sidebar w-72 shrink-0">
<div class="@min-[280px]/sidebar:grid-cols-2 grid gap-2">
<!-- "正在热播"小卡:参考侧栏宽度,页面再宽也是两列 -->
</div>
</aside>
<main class="@container/main flex-1">
<div class="grid gap-3 @2xl/main:grid-cols-2">
<!-- 本周新歌:参考主区宽度,主区够宽才分两列 -->
</div>
</main>
</div>
命名语法是两段:声明端 @container/sidebar,变体端 @md/sidebar:(斜杠后接名字)。规则有三条。其一,带名字的变体只看同名容器,彻底避开”最近祖先”的歧义;其二,不带名字的变体向上找最近的容器,就近原则在简单结构里很好用;其三,容器的 container-type: inline-size 会启用尺寸约束(宽度不能由其内容撑出),如果一个元素”宽度依赖内容、内容又依赖宽度”就会形成循环依赖,浏览器会给出怪异结果——@container-normal 类可以显式解除某个容器的约束,作为逃生门备用。
命名规范值得定成团队约定:容器名用”区域语义”而不是”组件名”——@container/sidebar、@container/main、@container/spotlight 说的是”这块区域多宽”,卡片名只出现在组件内部。这样多个组件共用同一容器时,变体里的名字不会随着重构改来改去;反过来,如果容器名跟着组件走,组件一挪位置,散落各处的 @md/xxx: 就全要改名。容器名是布局契约,稳定比贴切更重要。
变体刻度与设计规范的对应也值得整理:@xs(20rem)适合按钮组、徽章行这类微组件;@md(28rem)与 @lg(32rem)覆盖卡片与表单块的主流区间;@2xl 以上留给”聚光灯”式的大图区。把团队常用组件对应到刻度表,新卡片选断点就不必每次都试。
4. 与媒体查询对比:两条战线各管一段
| 维度 | 媒体查询 md: | 容器查询 @md: |
|---|---|---|
| 参考对象 | 视口宽度 | 最近(或指定)的容器宽度 |
| 断点刻度 | sm 40rem 起,md 48rem | @xs 20rem 起,@md 28rem(两套刻度) |
| 适用层级 | 页面级骨架(导航、分栏、页脚) | 组件级布局(卡片、表单块、列表项) |
| 复用性 | 组件挪到不同列要重写断点 | 组件自带逻辑,放到哪都对 |
| 浏览器支持 | 全量 | 2023 年起的现代浏览器 |
分工直觉一句话:页面骨架用媒体查询,内容组件用容器查询。页头导航、主侧分栏这类”整个视口才有意义”的决策仍归 md:;歌曲卡片、票档选择器、粉丝团徽章这类”跟着自己盒子走”的决策归 @md:。实践中会自然形成组件设计的新习惯——交付一个组件时不再问”它会被放在什么屏幕上”,因为组件自己知道答案。浏览器支持上,容器查询已成为 Baseline 特性,面向现代浏览器的产品可以直接使用,无需降级补丁。
调试容器查询有一个直观办法:在 DevTools 的 Elements 面板选中容器元素,Computed 面板里能看到 container-type: inline-size 是否生效;再配合布局视口高亮,“容器此刻多宽、命中了哪个变体”一眼可判。排查”变体没命中”时按顺序检查三件事:容器类是否存在、变体的名字与容器名是否一致、断点数值是否真的小于容器宽度——三层各自独立,缺一层都会静默失效。
迁移节奏上,存量项目的改法是”自底向上”:先把最通用的卡片、徽章组件换成容器变体并套上 @container,页面骨架保持不动;跑一段时间确认无回归后,再决定是否调整个别页面骨架。容器查询是增量可用的特性,不需要一次性全站切换。
5. 组件化实践:一份代码,三种位置通吃
把歌曲卡片抽成组件,验证”位置无关”的承诺。它会被放三个地方:280px 侧栏(竖排紧凑)、900px 主列表(横排舒展)、全宽”聚光灯”位(大图居中):
<!-- SongCard.astro 的标记结构(Astro 组件的模板部分) -->
<div class="@container">
<article
class="flex flex-col gap-3 @md:flex-row @md:items-center @4xl:flex-col @4xl:items-center"
>
<img
src={cover}
alt={`《${title}》封面`}
class="w-full rounded-md @md:w-40 @4xl:w-72"
/>
<div class="min-w-0">
<h3 class="text-base @lg:text-xl @4xl:text-2xl">{title}</h3>
<p class="text-sm text-text-secondary">P主:{producer}</p>
<p class="hidden text-sm @md:block">应援色:{themeColor}</p>
</div>
</article>
</div>
三个位置的宿主只需各自提供宽度(侧栏 w-72、主列表 flex-1、聚光灯 w-full),卡片内部根据自己拿到的实际宽度切换布局——侧栏里竖排紧凑,主列表里横排带专辑信息,聚光灯位在大容器下切回竖排大图。没有任何一行 JavaScript 参与判断,也不需要宿主传”layout=sidebar”之类的道具。
与状态驱动的类名(009 篇的 data-* 变体)可以叠加:data-[stock=0]:opacity-60 @md:data-[stock=0]:opacity-100 这样的复合变体让”余量售罄置灰”在窄容器里更强、宽容器里更含蓄。组件级响应式的终态就是:布局交给容器,状态交给属性,JS 只负责改数据。
在组件框架里(Astro、React、Vue)这套写法原样成立:容器类写在组件模板的根元素上,变体写在组件内部的类名里,组件被打包发布后,“自带响应式”随包分发——使用方不需要阅读组件源码就知道它会在窄容器里竖排。这是容器查询对组件库生态最大的改变:过去组件文档要写”在宽屏下使用效果最佳”的免责声明,现在组件自己负责到底。
性能角度容器查询还有一个隐性优点:切换发生在布局引擎内部,没有 JS 参与,组件数量再多也没有脚本开销;对比 ResizeObserver 方案(监听、防抖、批量改类),代码量与出错面都小一个量级。这也是”能用 CSS 就不用 JS”的老原则在响应式上的最新体现。
落地后别忘了回归基线:把组件在三种位置的页面截图存档(或纳入视觉回归工具),容器断点的调整才敢放开手脚——响应式组件的回归基线,就是”每个位置一张图”。
易错点与最佳实践
-
忘加
@container容器类。子元素上的@md:全部静默失效:<!-- 错误:没有容器,@md: 找不到参考系,样式不生效也不报错 --> <div> <p class="@md:text-lg">歌曲名</p> </div>修正:外层补上
@container,建立参考系。 -
把容器类加在宽度随内容变化的元素上。
container-type: inline-size会约束尺寸计算,行内元素或”内容撑宽”的盒子会出现宽度塌陷。修正:容器永远是”宽度由外部布局决定”的盒子(栅格列、固定宽侧栏、flex 子项配 min-w-0)。 -
嵌套结构里用裸变体命中了错误的祖先。三层容器嵌套时,不带名字的
@md:参考最近一层,常常不是你想的那层。修正:关键组件一律用命名容器@container/card+@md/card:,把参考系写死。 -
两套断点刻度混着心算。
@md是 28rem,md是 48rem,把容器变体的数值套用到媒体查询(或反过来)会得到差一倍的误判。修正:给团队一张对照表,或在命名上保持”容器变体一律带 @” 的视觉区分。 -
页面骨架也搬去容器查询。页头、分栏结构跟着视口走才是本意,套上容器查询反而要多余地铺容器。修正:骨架用
md:,组件用@md:,按第 4 节的分工执行。
本篇小结
- 容器查询把响应式参考系从视口换成父容器,补上了”组件不知道自己多大”的盲区。
@container声明容器(container-type: inline-size),@sm:系变体按容器宽度切换样式,任意值用@min-[520px]:。- 嵌套场景用命名容器消除歧义:
@container/sidebar声明、@md/sidebar:引用;@container-normal是解除约束的逃生门。 - 分工直觉:页面骨架用
md:媒体查询,内容组件用@md:容器查询;两套断点刻度独立,不可互套。 - 组件级响应式的终态是”布局交给容器、状态交给属性、JS 只改数据”,组件从此位置无关、自带适配。
动手实践
- 卡片三处通吃:按第 5 节实现 SongCard,分别放进 288px 侧栏、主列表与全宽聚光灯位,拖动窗口宽度验证三个位置各自按容器宽度切换,互不影响。提示:DevTools 里给宿主元素改宽度比改窗口更直观。
- 侧栏双列:给”正在热播”列表实现”容器 280px 单列、320px 以上双列”,对比用媒体查询实现的版本在窄窗口桌面上的错误表现。提示:体会参考系差异带来的行为差别。
- 命名容器的歧义实验:搭建三层嵌套容器,在最内层同时用裸
@md:与@md/inner:,逐层注释掉中间容器观察两者行为差异。提示:这一实验做完,“就近原则”就再也不会坑你。