容器查询
2025年3月2日大约 10 分钟
CSS容器查询
概述
CSS 容器查询是响应式设计的革命性进步,它允许开发者基于父容器的尺寸而非视口尺寸来应用样式。本文将详细介绍容器查询的基本概念、语法、实际应用场景以及最佳实践,帮助您创建更加灵活和可复用的组件。
目录
基本概念
什么是容器查询
容器查询允许开发者根据父容器的尺寸(而非整个视口的尺寸)来应用 CSS 样式。这意味着同一组件可以根据其所在容器的大小自动调整其布局和样式,而不受页面整体布局的影响。
容器查询与媒体查询的区别
特性 | 容器查询 | 媒体查询 |
---|---|---|
响应基准 | 父容器尺寸 | 视口尺寸 |
组件复用性 | 高(同一组件可在不同大小的容器中自适应) | 低(组件样式与视口绑定) |
上下文感知 | 是(组件知道其容器环境) | 否(组件只知道视口大小) |
嵌套组件 | 更适合处理 | 处理复杂 |
容器查询的优势
- 提高组件复用性:同一组件可以在不同大小的容器中自动调整布局
- 简化响应式设计:无需为每个视口尺寸编写媒体查询
- 更精确的上下文响应:组件可以根据其直接父容器做出响应
- 减少级联影响:组件样式更加独立,减少全局样式的影响
容器查询语法
定义容器
要使用容器查询,首先需要将元素定义为容器:
/* 定义内联容器(仅考虑宽度) */
.card-container {
container-type: inline-size;
}
/* 定义块级容器(考虑宽度和高度) */
.full-container {
container-type: size;
}
/* 定义容器并命名 */
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* 简写语法 */
.main-content {
container: content-area / inline-size;
}
查询容器
一旦定义了容器,就可以使用 @container
规则来查询容器尺寸:
/* 基于无名容器(最近的祖先容器)查询 */
@container (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}
/* 基于命名容器查询 */
@container sidebar (max-width: 300px) {
.widget {
font-size: 0.8rem;
}
}
/* 使用逻辑操作符 */
@container (min-width: 400px) and (max-width: 700px) {
.card-title {
font-size: 1.2rem;
}
}
容器查询单位
CSS 容器查询引入了新的相对单位,它们相对于查询容器的尺寸:
.responsive-element {
/* 相对于容器宽度的百分比 */
font-size: 5cqw;
/* 相对于容器高度的百分比 */
margin-top: 2cqh;
/* 相对于容器宽度或高度中较小值的百分比 */
padding: 1cqi;
/* 相对于容器宽度或高度中较大值的百分比 */
border-radius: 0.5cqb;
/* 相对于容器宽度和高度的几何平均值的百分比 */
margin-bottom: 2cqmin;
/* 相对于容器宽度和高度的算术平均值的百分比 */
width: 10cqmax;
}
实际应用
1. 响应式卡片组件
<div class="card-container">
<div class="card">
<img src="image.jpg" alt="Card image" class="card-image">
<div class="card-content">
<h3 class="card-title">卡片标题</h3>
<p class="card-description">卡片描述文本...</p>
</div>
</div>
</div>
.card-container {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.card-image {
width: 100%;
height: auto;
object-fit: cover;
}
/* 当容器宽度大于等于 500px 时 */
@container (min-width: 500px) {
.card {
flex-direction: row;
}
.card-image {
width: 40%;
}
.card-content {
width: 60%;
padding: 20px;
}
}
/* 当容器宽度小于 300px 时 */
@container (max-width: 300px) {
.card-title {
font-size: 1rem;
}
.card-description {
font-size: 0.8rem;
}
}
2. 自适应导航菜单
<div class="nav-container">
<nav class="navbar">
<div class="logo">Logo</div>
<ul class="nav-links">
<li><a href="#">首页</a></li>
<li><a href="#">关于</a></li>
<li><a href="#">服务</a></li>
<li><a href="#">联系</a></li>
</ul>
</nav>
</div>
.nav-container {
container-type: inline-size;
container-name: navbar;
}
.navbar {
display: flex;
flex-direction: column;
padding: 10px;
}
.nav-links {
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
}
/* 当导航容器宽度大于等于 600px 时 */
@container navbar (min-width: 600px) {
.navbar {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.nav-links {
flex-direction: row;
}
.nav-links li {
margin-left: 20px;
}
}
3. 自适应网格布局
<div class="grid-container">
<div class="grid">
<div class="grid-item">项目 1</div>
<div class="grid-item">项目 2</div>
<div class="grid-item">项目 3</div>
<div class="grid-item">项目 4</div>
<div class="grid-item">项目 5</div>
<div class="grid-item">项目 6</div>
</div>
</div>
.grid-container {
container-type: inline-size;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
.grid-item {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
}
/* 当容器宽度大于等于 400px 时 */
@container (min-width: 400px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 当容器宽度大于等于 700px 时 */
@container (min-width: 700px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
高级技巧
1. 嵌套容器查询
容器查询可以嵌套使用,创建多层次的响应式设计:
<div class="outer-container">
<div class="inner-container">
<div class="component">内容</div>
</div>
</div>
.outer-container {
container-type: inline-size;
container-name: outer;
}
.inner-container {
container-type: inline-size;
container-name: inner;
}
/* 基于外层容器的查询 */
@container outer (min-width: 800px) {
.inner-container {
padding: 20px;
}
}
/* 基于内层容器的查询 */
@container inner (min-width: 400px) {
.component {
display: flex;
}
}
2. 结合媒体查询和容器查询
媒体查询和容器查询可以结合使用,创建更复杂的响应式设计:
/* 首先基于视口设置基础布局 */
@media (min-width: 768px) {
.layout {
display: grid;
grid-template-columns: 1fr 3fr;
}
}
/* 然后基于容器调整组件样式 */
.component-container {
container-type: inline-size;
}
@container (min-width: 300px) {
.component {
display: flex;
}
}
3. 使用容器查询单位创建流体排版
.container {
container-type: inline-size;
}
.fluid-text {
/* 基础字体大小 */
font-size: 1rem;
/* 流体字体大小,随容器宽度变化 */
font-size: clamp(1rem, 0.5rem + 2cqw, 2rem);
}
.fluid-spacing {
/* 流体间距 */
padding: clamp(1rem, 0.5rem + 1cqi, 3rem);
}
浏览器兼容性
支持情况
浏览器 | 版本支持 |
---|---|
Chrome | 105+ |
Firefox | 110+ |
Safari | 16+ |
Edge | 105+ |
Opera | 91+ |
兼容性处理
对于不支持容器查询的浏览器,可以采用以下策略:
- 渐进增强:先提供基础样式,然后使用
@supports
检测容器查询支持:
/* 基础样式(所有浏览器) */
.card {
display: flex;
flex-direction: column;
}
/* 检测容器查询支持 */
@supports (container-type: inline-size) {
.card-container {
container-type: inline-size;
}
@container (min-width: 500px) {
.card {
flex-direction: row;
}
}
}
- 使用 JavaScript 回退方案:
// 检测容器查询支持
if (!CSS.supports('container-type', 'inline-size')) {
// 添加基于元素宽度的类
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const width = entry.contentRect.width;
entry.target.classList.remove('container-small', 'container-medium', 'container-large');
if (width < 300) {
entry.target.classList.add('container-small');
} else if (width < 600) {
entry.target.classList.add('container-medium');
} else {
entry.target.classList.add('container-large');
}
}
});
// 观察所有容器
document.querySelectorAll('.card-container').forEach(container => {
resizeObserver.observe(container);
});
}
最佳实践
1. 组件优先设计
容器查询特别适合组件优先的设计方法:
/* 定义组件容器 */
.component-wrapper {
container-type: inline-size;
}
/* 组件基础样式 */
.component {
/* 默认样式(小容器) */
}
/* 组件响应式变体 */
@container (min-width: 400px) {
.component {
/* 中等容器样式 */
}
}
@container (min-width: 700px) {
.component {
/* 大容器样式 */
}
}
2. 明智地选择容器类型
- 使用
container-type: inline-size
当只需要响应宽度变化时 - 使用
container-type: size
当需要响应宽度和高度变化时 - 注意
size
类型会影响性能,因为它需要监控两个维度的变化
3. 避免容器查询循环
容器查询可能导致布局循环,特别是当查询结果改变容器大小时:
/* 可能导致循环的代码 */
.container {
container-type: inline-size;
}
@container (min-width: 500px) {
.container {
width: 400px; /* 这会改变容器宽度,可能导致循环 */
}
}
4. 结合使用容器查询和 CSS 网格
容器查询与 CSS 网格配合使用效果很好,可以创建高度自适应的布局:
.grid-container {
container-type: inline-size;
}
.grid {
display: grid;
gap: 20px;
/* 基础网格:单列 */
grid-template-columns: 1fr;
}
@container (min-width: 400px) {
.grid {
/* 中等容器:两列,不同比例 */
grid-template-columns: 2fr 1fr;
}
}
@container (min-width: 700px) {
.grid {
/* 大容器:三列均等 */
grid-template-columns: repeat(3, 1fr);
}
}
/* 使用 grid-template-areas 重新排列内容 */
@container (min-width: 900px) {
.grid {
grid-template-columns: repeat(4, 1fr);
grid-template-areas:
"main main main sidebar"
"feat1 feat2 feat3 sidebar";
}
.main-content { grid-area: main; }
.sidebar { grid-area: sidebar; }
.feature-1 { grid-area: feat1; }
.feature-2 { grid-area: feat2; }
.feature-3 { grid-area: feat3; }
}
5. 使用样式封装
容器查询特别适合与 CSS 样式封装技术(如 Shadow DOM)结合使用:
<my-component>
#shadow-root
<div class="container">
<div class="content">...</div>
</div>
<style>
.container {
container-type: inline-size;
}
@container (min-width: 400px) {
.content {
display: flex;
}
}
</style>
</my-component>
实际案例分析
案例 1:自适应产品卡片
<div class="products-grid">
<div class="product-container">
<div class="product-card">
<div class="product-image">
<img src="product.jpg" alt="产品图片">
</div>
<div class="product-info">
<h3 class="product-title">产品名称</h3>
<p class="product-description">产品描述文本,详细介绍产品特点和优势...</p>
<div class="product-meta">
<span class="product-price">¥299</span>
<button class="add-to-cart">加入购物车</button>
</div>
</div>
</div>
</div>
<!-- 更多产品卡片 -->
</div>
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.product-container {
container-type: inline-size;
}
.product-card {
display: flex;
flex-direction: column;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
}
.product-image {
width: 100%;
}
.product-image img {
width: 100%;
height: auto;
object-fit: cover;
aspect-ratio: 16/9;
}
.product-info {
padding: 15px;
}
.product-description {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-meta {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
.product-price {
font-size: 1.2rem;
font-weight: bold;
color: #e44d26;
}
.add-to-cart {
padding: 8px;
background-color: #4a90e2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* 容器查询:中等宽度容器 */
@container (min-width: 350px) {
.product-meta {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
/* 容器查询:宽容器 */
@container (min-width: 500px) {
.product-card {
flex-direction: row;
}
.product-image {
width: 40%;
}
.product-image img {
height: 100%;
aspect-ratio: auto;
}
.product-info {
width: 60%;
display: flex;
flex-direction: column;
}
.product-description {
-webkit-line-clamp: 3;
flex-grow: 1;
}
}
案例 2:自适应仪表板组件
<div class="dashboard">
<div class="widget-container">
<div class="widget">
<h3 class="widget-title">销售统计</h3>
<div class="widget-content">
<div class="chart-container">
<!-- 图表内容 -->
</div>
<div class="stats-list">
<div class="stat-item">
<span class="stat-label">总销售额</span>
<span class="stat-value">¥128,450</span>
</div>
<div class="stat-item">
<span class="stat-label">订单数</span>
<span class="stat-value">1,024</span>
</div>
<div class="stat-item">
<span class="stat-label">客户数</span>
<span class="stat-value">512</span>
</div>
</div>
</div>
</div>
</div>
<!-- 更多组件 -->
</div>
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.widget-container {
container-type: inline-size;
}
.widget {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 15px;
}
.widget-title {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.widget-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.chart-container {
height: 200px;
background-color: #f9f9f9;
border-radius: 4px;
}
.stats-list {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.stat-item {
display: flex;
flex-direction: column;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.stat-value {
font-size: 1.2rem;
font-weight: bold;
}
/* 容器查询:中等宽度容器 */
@container (min-width: 400px) {
.stats-list {
grid-template-columns: repeat(3, 1fr);
}
}
/* 容器查询:宽容器 */
@container (min-width: 600px) {
.widget-content {
flex-direction: row;
}
.chart-container {
width: 60%;
height: auto;
}
.stats-list {
width: 40%;
grid-template-columns: 1fr;
}
}
参考资源
总结
CSS 容器查询是响应式设计的重要进步,它解决了传统媒体查询的局限性,使组件能够根据其容器环境而非整个视口做出响应。这种方法带来了更高的组件复用性和更精确的上下文响应能力。
关键要点:
- 组件自适应:同一组件可以在不同大小的容器中自动调整布局和样式
- 上下文感知:组件可以根据其直接父容器的尺寸做出响应
- 提高复用性:设计一次,在任何容器中都能自动适应
- 与媒体查询互补:媒体查询处理整体布局,容器查询处理组件内部布局
- 新的相对单位:容器查询单位(如 cqw、cqh)提供了更精确的相对尺寸控制
随着浏览器支持的不断完善,容器查询将成为前端开发中不可或缺的工具,帮助开发者创建更加灵活、可维护和可复用的组件。