前置知识: Tailwind CSS

容器查询

11 min中级

Tailwind CSS 容器查询:@container 声明参考系、@sm:/@md: 容器变体与媒体查询断点对照、命名容器消除嵌套歧义,组件级响应式实践

响应式设计的老工具是媒体查询:视口宽度小于某个值就换布局。但它有个盲区——组件不知道自己被放在多大的盒子里。同一张歌曲卡片,放在 280px 的侧栏要竖排,放在 900px 的主列表要横排,媒体查询只能”猜视口”,猜不对就出事。容器查询(Tailwind 4 已内置)把响应式的参考系从视口换成组件的父容器,组件从此自带响应式逻辑。

前置知识

学习目标

  1. 能说出媒体查询的盲区在哪里,解释容器查询补上了什么。
  2. 能用 @container 标注容器,用 @sm: 系变体按容器宽度切换样式。
  3. 能为嵌套容器命名(@container/sidebar)并用 @md/sidebar: 指定参考系。
  4. 能对比媒体查询与容器查询的适用层级,形成”页面用 md:、组件用 @md:“的分工直觉。
  5. 能把歌曲卡片做成”一份代码、三种位置通吃”的真响应式组件。

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”的老原则在响应式上的最新体现。

落地后别忘了回归基线:把组件在三种位置的页面截图存档(或纳入视觉回归工具),容器断点的调整才敢放开手脚——响应式组件的回归基线,就是”每个位置一张图”。

易错点与最佳实践

  1. 忘加 @container 容器类。子元素上的 @md: 全部静默失效:

    <!-- 错误:没有容器,@md: 找不到参考系,样式不生效也不报错 -->
    <div>
      <p class="@md:text-lg">歌曲名</p>
    </div>

    修正:外层补上 @container,建立参考系。

  2. 把容器类加在宽度随内容变化的元素上。container-type: inline-size 会约束尺寸计算,行内元素或”内容撑宽”的盒子会出现宽度塌陷。修正:容器永远是”宽度由外部布局决定”的盒子(栅格列、固定宽侧栏、flex 子项配 min-w-0)。

  3. 嵌套结构里用裸变体命中了错误的祖先。三层容器嵌套时,不带名字的 @md: 参考最近一层,常常不是你想的那层。修正:关键组件一律用命名容器 @container/card + @md/card:,把参考系写死。

  4. 两套断点刻度混着心算。@md 是 28rem,md 是 48rem,把容器变体的数值套用到媒体查询(或反过来)会得到差一倍的误判。修正:给团队一张对照表,或在命名上保持”容器变体一律带 @” 的视觉区分。

  5. 页面骨架也搬去容器查询。页头、分栏结构跟着视口走才是本意,套上容器查询反而要多余地铺容器。修正:骨架用 md:,组件用 @md:,按第 4 节的分工执行。

本篇小结

  1. 容器查询把响应式参考系从视口换成父容器,补上了”组件不知道自己多大”的盲区。
  2. @container 声明容器(container-type: inline-size),@sm: 系变体按容器宽度切换样式,任意值用 @min-[520px]:。
  3. 嵌套场景用命名容器消除歧义:@container/sidebar 声明、@md/sidebar: 引用;@container-normal 是解除约束的逃生门。
  4. 分工直觉:页面骨架用 md: 媒体查询,内容组件用 @md: 容器查询;两套断点刻度独立,不可互套。
  5. 组件级响应式的终态是”布局交给容器、状态交给属性、JS 只改数据”,组件从此位置无关、自带适配。

动手实践

  1. 卡片三处通吃:按第 5 节实现 SongCard,分别放进 288px 侧栏、主列表与全宽聚光灯位,拖动窗口宽度验证三个位置各自按容器宽度切换,互不影响。提示:DevTools 里给宿主元素改宽度比改窗口更直观。
  2. 侧栏双列:给”正在热播”列表实现”容器 280px 单列、320px 以上双列”,对比用媒体查询实现的版本在窄窗口桌面上的错误表现。提示:体会参考系差异带来的行为差别。
  3. 命名容器的歧义实验:搭建三层嵌套容器,在最内层同时用裸 @md: 与 @md/inner:,逐层注释掉中间容器观察两者行为差异。提示:这一实验做完,“就近原则”就再也不会坑你。