欢迎访问晨星博客!
还在用媒体查询让组件“看视口脸色”?这大概是很多前端开发者在维护老项目时最想吐槽的痛点。一个卡片组件,放在主内容区是三列网格,塞进侧边栏就得变单列堆叠,放进模态框里又得压缩成紧凑模式。为了应对这些场景,我们不得不写一堆 .card--sidebar、.card--modal 这样的修饰类,或者在 JavaScript 里计算容器宽度动态加类名。代码越写越散,复用性越来越低。
容器查询的出现,正是为了解决这个“组件不知道自己在哪、只能看视口大小”的结构性矛盾。它让组件能感知最近的祖先容器尺寸,从而根据自身所在空间自主决定布局。这不是锦上添花,而是组件化开发缺失的一环。

媒体查询监听的是视口宽度。这在页面级布局时没问题——导航栏折叠、侧边栏收起、字号调整,这些确实该跟随视口变化。但组件层面不一样。
假设有一个产品卡片:
/* 传统写法:只能盯着视口 */
.product-card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.product-card {
grid-template-columns: 200px 1fr;
}
}
当视口 ≥ 768px 时,卡片变成横向两列。问题来了:
开发者不得不引入修饰类或 JS 计算:
/* 无奈的补丁方案 */
.product-card--compact {
grid-template-columns: 1fr;
}
.product-card--horizontal {
grid-template-columns: 200px 1fr;
}
这违背了组件封装的初衷——组件不该关心自己被谁用、放在哪,它只该关心自己有多少空间可用。
容器查询只需三个声明:
.card-container {
/* 1. 声明这是一个查询容器 */
container-type: inline-size;
/* 可选:给容器起名,便于嵌套时精准定位 */
container-name: card-wrapper;
}
@container card-wrapper (min-width: 400px) {
/* 2. 当容器宽度 ≥ 400px 时生效 */
.product-card {
grid-template-columns: 200px 1fr;
}
}
关键点:
| 概念 | 作用 |
|---|---|
container-type: inline-size | 监听容器内联方向尺寸(宽度),这是最常用的;还有 size 同时监听宽高,normal 关闭查询 |
container-name | 可选,解决嵌套容器时“查到错容器”的问题 |
@container | 语法与 @media 几乎一致,支持 min-width、max-width、范围写法 (300px <= width < 600px) |
浏览器支持:Chrome 105+、Firefox 119+、Safari 16.4+,全球可用率已超 92%。旧版浏览器会直接忽略 @container 块,配合 @supports 做渐进增强即可,后文再说。
<!-- 主内容区 -->
<section class="main-content">
<div class="card-wrapper">
<article class="product-card">...</article>
</div>
</section>
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="card-wrapper">
<article class="product-card">...</article>
</div>
</aside>
/* 旧版:只能二选一,或靠修饰类硬拆 */
.product-card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
@media (min-width: 768px) {
.product-card {
grid-template-columns: 180px 1fr;
}
}
/* 1. 容器声明:所有包裹卡片的父元素 */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* 2. 基础态:最窄时的单列堆叠 */
.product-card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
/* 3. 容器 ≥ 350px:图文横向,图片固定宽度 */
@container card (min-width: 350px) {
.product-card {
grid-template-columns: 140px 1fr;
align-items: start;
}
}
/* 4. 容器 ≥ 550px:更宽松的两列,标题字号放大 */
@container card (min-width: 550px) {
.product-card {
grid-template-columns: 200px 1fr;
gap: 1.5rem;
padding: 1.5rem;
}
.product-card__title {
font-size: 1.25rem;
}
}
效果:
同一个 .product-card,零修饰类,零 JS,三种容器三种形态。这就是“组件级自适应”的含义。
侧边栏常见需求:宽时显示图标+文字,窄时只显示图标,极窄时折叠成抽屉。用媒体查询写死断点,一旦页面布局调整(如主内容区变宽、侧边栏变窄),断点全得改。容器查询让导航只管自己容器宽度:
.sidebar-nav {
container-type: inline-size;
container-name: sidebar;
}
.nav-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition: background 0.2s;
}
.nav-item__label {
white-space: nowrap;
opacity: 1;
transition: opacity 0.2s, width 0.2s;
}
/* 容器 ≥ 200px:显示文字 */
@container sidebar (min-width: 200px) {
.nav-item__label {
width: auto;
opacity: 1;
}
}
/* 容器 < 200px:只显示图标,文字隐藏但保留无障碍读屏 */
@container sidebar (max-width: 199px) {
.nav-item__label {
width: 0;
opacity: 0;
overflow: hidden;
}
.nav-item {
justify-content: center;
padding: 0.5rem;
}
}
配合 CSS :has() 或 JS 切换容器宽度类(如 .sidebar--collapsed { width: 64px; }),导航就能在“展开/折叠/响应式收缩”三态间平滑切换,完全不关心视口宽度。

| 维度 | 媒体查询 + 修饰类 | 容器查询 |
|---|---|---|
| 组件耦合度 | 高(组件需要知道页面断点、需要知道自己被谁用) | 低(组件只关心自身容器尺寸) |
| 新增布局场景 | 改组件 CSS、加修饰类、改调用处 HTML | 只需在新容器上加 container-type |
| 维护成本 | 断点变动牵一发动全身 | 断点内聚在组件内部,互不干扰 |
| 设计系统友好度 | 差(Token 无法覆盖“容器上下文”) | 好(组件自带响应式契约) |
| SSR/静态生成 | 需要运行时 JS 判断或多套模板 | 纯 CSS,构建时确定,零运行时开销 |
一句话总结:媒体查询适合页面级宏观布局(导航折叠、全局字号、栅格列数);容器查询适合组件级微观自适应(卡片形态、表单密度、导航收折、图表尺寸)。
@supports 渐进增强(推荐,零依赖)/* 基础态:所有浏览器都能跑 */
.product-card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* 现代浏览器:容器查询增强 */
@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 350px) {
.product-card {
grid-template-columns: 140px 1fr;
}
}
@container card (min-width: 550px) {
.product-card {
grid-template-columns: 200px 1fr;
}
}
}
不支持的浏览器永远看到单列基础态,功能完整、视觉可用、无布局崩坏。这是最稳妥的策略。
npm install @container-query/polyfill
import '@container-query/polyfill';
// 或在 HTML 引入 CDN
// <script src="https://cdn.jsdelivr.net/npm/@container-query/polyfill"></script>
Polyfill 基于 ResizeObserver 模拟容器查询,有性能开销(每个容器一个 Observer),且不支持 container-name 嵌套选择。生产环境建议仅在必须支持 Safari 15 / 旧版 Edge 的项目中按需引入,并配合 browserslist 仅对目标环境注入。
@media 兜底 + 容器查询增强(折中)/* 兜底:视口 ≥ 768px 时给个大致横向布局 */
@media (min-width: 768px) {
.product-card {
grid-template-columns: 180px 1fr;
}
}
/* 增强:容器查询精准接管 */
@supports (container-type: inline-size) {
@media (min-width: 768px) {
.product-card {
grid-template-columns: 1fr; /* 重置,交给容器查询决定 */
}
}
.card-wrapper { container-type: inline-size; }
@container (min-width: 350px) { ... }
}
思路:先用媒体查询给个“凑合能用”的默认态,再用容器查询在支持的环境里精准覆盖。既不写 Polyfill,又不让旧浏览器完全退化到单列。
container-type → 查询静默失效,控制台无报错。养成习惯:写 @container 前先检查祖先链是否有 container-type。container-type: inline-size 只监听宽度;若需监听高度(如垂直折叠面板),用 container-type: size,但会触发更多布局计算,性能稍差。container-type 会“截胡”查询。必须用 container-name 显式指定:@container card (min-width: 400px)。:root 或 body 上加 container-type 监听视口,完全失去意义,还不如直接写 @media。@container 断点最好用 CSS 变量暴露:--card-breakpoint-sm: 350px,让使用方能按设计系统统一调整。/* 组件库内部 */
.product-card {
--card-bp-sm: 350px;
--card-bp-lg: 550px;
}
@container card (min-width: var(--card-bp-sm)) { ... }
@container card (min-width: var(--card-bp-lg)) { ... }
/* 使用方覆盖 */
.my-special-layout .card-wrapper {
--card-bp-sm: 300px;
--card-bp-lg: 500px;
}
container-type: inline-size@container,不写 @media@supports 做渐进增强,旧浏览器保底单列,现代浏览器享受自适应。@container style(...):容器查询还能查容器的计算样式(如 container-query: style(--theme: dark)),这是下一步可以探索的“主题感知组件”能力。容器查询不是新玩具,是组件化开发缺失十年的基础设施。把它用上,你会发现很多曾经需要 JS、修饰类、甚至重写组件才能解决的“放置位置导致的布局问题”,在 CSS 层面就彻底闭环了。
下次写组件时,先问自己:“这个组件如果被塞进侧边栏、模态框、多列网格,它自己能搞定吗?” 能,就加 container-type;不能,才考虑别的方案。
声明:原创文章请勿转载,如需转载请注明出处!