问题场景
你有没有遇到过这种情况:写了一个精美的卡片组件,在首页大屏上完美展示,结果放到侧边栏小区域里时,布局全崩了?
css
/* 传统媒体查询------只认视口,不认容器 */
.card {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
/* ❌ 当卡片放在 300px 宽的侧边栏时,两列依然生效 */
/* 只能通过加类名或写一堆 hack 来处理 */
@media (max-width: 768px) {
.card {
grid-template-columns: 1fr; /* 但这是看视口宽度的!不是容器宽度!*/
}
}
传统 @media 查询只关心浏览器视口宽度。当你的组件被塞进侧边栏、弹窗、折叠面板里时,视口可能还是 1920px,但组件实际可用宽度只剩 400px------这个时候布局怎么调?加 class?用 resize observer JS 算?又脏又累。
容器查询(Container Queries) 正是为了解决这个问题而生------让组件根据自身容器的宽度来决定布局。
原因分析
组件级的响应式 vs 页面级的响应式
@media 是为页面布局设计的,它回答的是"视口多宽?"。而现代前端组件化开发中,一个组件可能出现在页面的任何位置------主内容区 1200px,侧边栏 320px,Modal 里 600px,Split Panel 左侧 400px。
如果你的组件写死了 @media (max-width: 768px) 来切换布局,那它在侧边栏里永远都是"小屏模式",即使视口有 4K 宽。👇
javascript
// ❌ 传统方案的狼狈
function SidebarCard() {
// 还得额外监测容器宽度,自己算
return <div className="sidebar-card card--compact">...</div>
}
关键是 containment
CSS Container Queries 的核心是 containment(包含性) 。你通过 container-type 告诉浏览器:"这个元素是一个查询容器,它的子元素可以查我的尺寸。" 浏览器因此知道它需要为这个容器维护独立的尺寸信息,这就是 contain 属性的延伸。
解决方案
基础用法三步走
第一步:声明容器
在你想查询的父元素上设置容器:
css
.card-container {
container-type: inline-size;
/* 或者简写:container: inline-size; */
/* 也可以起个名字,方便多个容器区分 */
container-name: sidebar;
}
第二步:写容器查询
css
/* 默认:大容器布局------两列 */
.card {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 20px;
background: #fff;
border-radius: 12px;
}
/* 当容器宽度 < 500px 时------变为单列 */
@container (max-width: 500px) {
.card {
grid-template-columns: 1fr;
}
.card__image {
width: 100%;
height: 200px;
}
}
/* 当容器宽度 < 300px 时------极致紧凑模式 */
@container (max-width: 300px) {
.card {
grid-template-columns: 1fr;
gap: 8px;
padding: 12px;
}
.card__title {
font-size: 14px;
}
.card__desc {
display: none; /* 空间太小,隐藏描述 */
}
}
第三步:用上就完事
html
<!-- 同一组件,不同容器,自动适配 -->
<div class="main-content">
<div class="card-container">
<div class="card">
<div class="card__image"><img src="..." alt="" /></div>
<div class="card__body">
<h3 class="card__title">标题</h3>
<p class="card__desc">描述信息...</p>
</div>
</div>
</div>
</div>
<aside class="sidebar">
<div class="card-container">
<div class="card">
<!-- 同样的结构,自动触发紧凑模式 -->
</div>
</div>
</aside>
实操:一个完整的响应式商品卡片
来看一个真实场景------商品卡片组件,它既能放在首页网格(宽),也能放在推荐侧边栏(窄),还能在弹窗里展示:
html
<div class="product-grid">
<div class="product-wrapper">
<div class="product-card">
<img class="product-card__img" src="product.jpg" alt="商品" />
<div class="product-card__info">
<h4 class="product-card__name">智能手表 Pro Max</h4>
<p class="product-card__price">¥2,999</p>
<p class="product-card__rating">★★★★☆ 4.5</p>
<button class="product-card__btn">加入购物车</button>
</div>
</div>
</div>
</div>
css
/* 声明容器 */
.product-wrapper {
container-type: inline-size;
container-name: product;
}
/* 默认布局:水平排列(宽容器) */
.product-card {
display: flex;
gap: 16px;
padding: 16px;
border: 1px solid #eee;
border-radius: 12px;
background: #fff;
transition: all 0.2s;
}
.product-card__img {
width: 160px;
height: 160px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
/* 容器宽度 ≤ 480px → 改为垂直排列 */
@container product (max-width: 480px) {
.product-card {
flex-direction: column;
}
.product-card__img {
width: 100%;
height: 200px;
}
}
/* 容器宽度 ≤ 300px → 极简模式 */
@container product (max-width: 300px) {
.product-card {
flex-direction: column;
padding: 10px;
}
.product-card__rating {
display: none;
}
.product-card__img {
height: 140px;
}
.product-card__btn {
width: 100%;
font-size: 13px;
padding: 8px;
}
}
在 React / Vue 中的最佳实践
React 示例:
tsx
// ProductCard.tsx
import './ProductCard.css'
// 容器包裹层负责声明 container-type
function ProductWrapper({ children }: { children: React.ReactNode }) {
return <div className="product-wrapper">{children}</div>
}
// 卡片组件只管写 @container,完全无感知
function ProductCard({ product }: { product: Product }) {
return (
<div className="product-card">
<img className="product-card__img" src={product.image} alt="" />
<div className="product-card__info">
<h4>{product.name}</h4>
<p className="product-card__price">{product.price}</p>
<button>加入购物车</button>
</div>
</div>
)
}
// 使用:任何地方用 ProductWrapper 包裹即可自适应
function HomePage() {
return (
<div className="home-layout">
{/* 主区域:宽容器 */}
<section className="main-grid">
{products.map(p => (
<ProductWrapper key={p.id}>
<ProductCard product={p} />
</ProductWrapper>
))}
</section>
{/* 侧边栏:窄容器,自动触发紧凑布局 */}
<aside className="sidebar">
<h3>推荐商品</h3>
{hotProducts.map(p => (
<ProductWrapper key={p.id}>
<ProductCard product={p} />
</ProductWrapper>
))}
</aside>
</div>
)
}
Vue 示例:
vue
<!-- ProductContainer.vue -->
<template>
<div class="product-container">
<slot />
</div>
</template>
<style scoped>
.product-container {
container-type: inline-size;
}
</style>
<!-- 使用 -->
<ProductContainer>
<ProductCard :product="item" />
</ProductContainer>
浏览器兼容性与降级
截至 2026 年 7 月,Container Queries 的基础功能已得到所有现代浏览器支持(Chrome 105+、Firefox 110+、Safari 16+)。如果你需要兼容老浏览器,用 polyfill:
bash
npm install container-query-polyfill
然后在入口文件引入:
typescript
// 只在需要时加载 polyfill
import 'container-query-polyfill'
推荐用 渐进增强 策略:先写基于 @container 的现代化布局,降级方案用 flex/grid 的自然折行行为兜底:
css
.product-card {
/* ✅ 降级:flex-wrap 保证至少不崩 */
display: flex;
flex-wrap: wrap;
}
/* ✅ 增强:支持容器查询的浏览器获得更精确控制 */
@container (min-width: 500px) {
.product-card {
flex-wrap: nowrap;
}
}
要点总结
| 对比 | @media 媒体查询 |
@container 容器查询 |
|---|---|---|
| 查询基准 | 浏览器视口 | 父容器尺寸 |
| 适用场景 | 页面级布局 | 组件级布局 |
| 组件复用性 | ❌ 依赖上下文 | ✅ 完全自治 |
| 维护成本 | 越写越乱 | 组件自包含 |
- 父元素声明 :用
container-type: inline-size或container: <name> / inline-size声明容器 - 子元素查询 :用
@container (条件)代替@media查询容器宽度 - 命名容器 :多个容器用
container-name区分,查询时@container <name> (条件) - 渐进增强:用 flex/grid 折行做兜底,支持容器查询的浏览器获得更优体验
- 性能友好 :
container-type触发 CSS containment,浏览器会优化重排计算