CSS容器查询(Container Queries)是CSS领域最受期待的新特性之一。它让组件可以根据自身容器的大小来调整样式,而不是根据视口大小。这意味着,真正的组件级响应式设计成为可能。本文详细介绍CSS容器查询,从基本概念到高级用法,包括语法、配置、使用场景、浏览器支持、降级方案等。如果你对响应式设计感兴趣,希望这篇文章能帮你了解这个激动人心的新特性。

一、为什么需要容器查询

在介绍容器查询之前,先说说为什么需要它。

1. 媒体查询的局限

现在的响应式设计,主要是通过媒体查询(Media Queries)来实现的。媒体查询根据视口(viewport)的大小来应用不同的样式。

比如:

.card {
  display: flex;
  flex-direction: column;
}

@media (min-width: 768px) {
  .card {
    flex-direction: row;
  }
}

这段代码的意思是,当视口宽度大于等于768px时,卡片的布局从纵向变成横向。

媒体查询在页面级的响应式设计中,工作得很好。但在组件级的响应式设计中,就有很大的局限。

2. 组件级响应式的需求

在现代前端开发中,组件化是主流。我们把页面拆分成一个个组件,每个组件可以在不同的地方复用。

比如,一个卡片组件,可以用在页面的主内容区,也可以用在侧边栏,还可以用在弹窗里。在不同的地方,组件的可用宽度是不一样的。

在主内容区,卡片可能有800px宽,可以用横向布局;在侧边栏,卡片可能只有300px宽,需要用纵向布局。

但用媒体查询,我们无法根据组件自身容器的大小来调整样式,只能根据视口大小。这就导致,同样的组件,在视口宽度相同的情况下,不管放在主内容区还是侧边栏,样式都是一样的。这显然不符合我们的需求。

这就是媒体查询的局限:它只能感知视口的大小,不能感知组件容器的大小。

3. 容器查询的解决方案

容器查询就是为了解决这个问题而生的。

容器查询允许组件根据自身容器的大小来应用不同的样式。你可以给一个元素定义为容器,然后它的子元素可以根据这个容器的大小来应用样式。

这样,同样的卡片组件,放在宽的容器里,就用横向布局;放在窄的容器里,就用纵向布局。真正实现了组件级的响应式设计。

容器查询,被很多前端开发者称为"CSS的圣杯"。因为它解决了响应式设计中一个长期存在的痛点,让组件真正可以做到"一次编写,到处使用",不管放在什么容器里,都能自适应。

二、容器查询的基本概念

在深入学习容器查询的语法之前,先了解一些基本概念。

1. 容器(Container)

容器,就是被定义为可以被查询的元素。一个元素被定义为容器之后,它的子元素就可以根据它的大小来应用样式。

定义容器,需要用container-type属性,指定容器的类型。目前支持的类型有:

  • size:查询容器的宽度和高度
  • inline-size:只查询容器的行内方向(水平方向)的大小
  • normal:默认值,不是查询容器

最常用的是inline-size,因为大部分时候,我们只需要根据容器的宽度来调整样式。

2. 容器查询(Container Query)

容器查询,就是根据容器的大小来应用样式。用@container规则来定义,语法和媒体查询类似。

比如:

@container (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
}

这段代码的意思是,当容器的宽度大于等于400px时,卡片用横向布局。

容器查询的条件,可以用min-widthmax-widthmin-heightmax-height等,和媒体查询类似。

3. 容器名称(Container Name)

如果页面上有多个容器,可以给容器起名字,这样在容器查询中,可以指定查询哪个容器。

container-name属性给容器起名字:

.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

然后在容器查询中指定容器名称:

@container sidebar (min-width: 300px) {
  .card {
    /* styles */
  }
}

如果不指定容器名称,容器查询会查询最近的祖先容器。

4. 容器查询长度单位(CQW/CQH/CQI/CQB)

容器查询还引入了新的长度单位,相对于容器的大小:

  • cqw:容器宽度的1%
  • cqh:容器高度的1%
  • cqi:容器行内方向尺寸的1%
  • cqb:容器块级方向尺寸的1%
  • cqmin:cqw和cqh中较小的那个
  • cqmax:cqw和cqh中较大的那个

这些单位,类似于视口单位(vw/vh),但是相对于容器的大小,而不是视口的大小。

有了这些单位,我们可以让组件的字体大小、间距等,根据容器的大小来缩放,实现更灵活的响应式设计。

三、容器查询的基本用法

了解了基本概念之后,来看看容器查询的基本用法。

1. 定义容器

使用容器查询的第一步,是定义一个容器。

container-type属性定义容器:

.card-container {
  container-type: inline-size;
}

这样,.card-container就成为了一个容器,它的子元素可以根据它的宽度来应用样式。

也可以用container简写属性,同时定义类型和名称:

.card-container {
  container: card / inline-size;
}

这相当于:

.card-container {
  container-name: card;
  container-type: inline-size;
}

2. 编写容器查询

定义了容器之后,就可以用@container编写容器查询了。

@container (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
  .card-image {
    width: 40%;
  }
  .card-content {
    width: 60%;
  }
}

@container (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}

这段代码的意思是,当容器宽度大于等于400px时,卡片用横向布局,图片占40%,内容占60%;当容器宽度小于400px时,卡片用纵向布局。

这样,同样的卡片组件,放在宽的容器里就是横向布局,放在窄的容器里就是纵向布局,真正实现了组件级的响应式。

3. 使用容器查询长度单位

除了用@container规则,还可以用容器查询长度单位,让样式更灵活。

.card-title {
  font-size: clamp(1rem, 4cqi, 2rem);
  margin-bottom: 2cqi;
}

.card-content {
  font-size: clamp(0.875rem, 3cqi, 1.125rem);
  line-height: 1.5;
}

这段代码用了cqi单位(容器行内方向尺寸的1%),配合clamp()函数,让字体大小和间距,根据容器的大小来缩放。

容器宽的时候,字体大一些,间距大一些;容器窄的时候,字体小一些,间距小一些。这样,组件在不同大小的容器里,都能有合适的排版。

4. 嵌套容器查询

容器查询也可以嵌套,实现更复杂的响应式逻辑。

@container (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
  
  @container (min-width: 600px) {
    .card {
      gap: 2rem;
    }
    .card-title {
      font-size: 1.5rem;
    }
  }
}

这段代码的意思是,当容器宽度大于等于400px时,卡片用横向布局;当容器宽度大于等于600px时,在横向布局的基础上,增加间距,增大标题字体。

嵌套容器查询,可以实现更精细的响应式控制。

四、容器查询的高级用法

掌握了基本用法之后,来看看一些高级用法。

1. 多容器查询

一个组件,可能同时受到多个容器的影响。这时候,可以用多个容器查询,分别处理不同容器的情况。

/* 主内容区容器 */
.main-content {
  container-type: inline-size;
  container-name: main;
}

/* 侧边栏容器 */
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

/* 在主内容区的样式 */
@container main (min-width: 600px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1.5rem;
  }
}

/* 在侧边栏的样式 */
@container sidebar (min-width: 300px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}

@container sidebar (max-width: 299px) {
  .card {
    display: flex;
    flex-direction: column;
    font-size: 0.875rem;
  }
}

这样,同样的卡片组件,在主内容区和侧边栏,会有完全不同的样式,各自适应自己的容器。

2. 容器查询和媒体查询结合使用

容器查询和媒体查询,可以结合使用,实现更全面的响应式设计。

媒体查询处理页面级的响应式(比如整体布局、导航栏等),容器查询处理组件级的响应式(比如卡片、按钮、表单等)。

/* 页面级响应式:媒体查询 */
@media (min-width: 768px) {
  .layout {
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: 2rem;
  }
}

/* 组件级响应式:容器查询 */
.main-content {
  container-type: inline-size;
}

@container (min-width: 500px) {
  .article-card {
    display: flex;
    flex-direction: row;
  }
}

这样,页面的整体布局用媒体查询控制,组件的样式用容器查询控制,两者结合,实现真正的响应式设计。

3. 容器查询和CSS变量结合

容器查询可以和CSS变量结合,实现更灵活的样式控制。

.card-container {
  container-type: inline-size;
}

.card {
  --card-gap: 1rem;
  --card-title-size: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}

.card-title {
  font-size: var(--card-title-size);
}

@container (min-width: 400px) {
  .card {
    --card-gap: 1.5rem;
    --card-title-size: 1.5rem;
    flex-direction: row;
  }
}

@container (min-width: 600px) {
  .card {
    --card-gap: 2rem;
    --card-title-size: 1.75rem;
  }
}

这样,通过在容器查询中修改CSS变量的值,可以一次性修改多个样式属性,代码更简洁,也更容易维护。

4. 容器查询中的内容可见性优化

容器查询还可以和content-visibility结合,优化页面性能。

content-visibility是另一个新的CSS特性,可以让浏览器跳过不在视口内的元素的渲染,提升页面加载速度。

.card-container {
  container-type: inline-size;
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;
}

这样,容器查询和内容可见性优化结合,既实现了组件级响应式,又提升了页面性能。

五、使用场景和最佳实践

了解了容器查询的用法之后,来看看它的使用场景和最佳实践。

1. 适合使用容器查询的场景

容器查询特别适合以下场景:

  • 可复用组件:需要在不同地方复用的组件,比如卡片、按钮、表单、导航等。这些组件在不同的容器里,需要不同的样式
  • 组件库设计:设计组件库的时候,容器查询可以让组件真正做到"一次编写,到处使用",不管放在什么容器里,都能自适应
  • 复杂布局:复杂的页面布局,有多个区域,每个区域的组件需要根据区域大小来调整样式
  • 设计系统:设计系统中,组件需要在不同的上下文中使用,容器查询可以让组件更好地适应不同的上下文

2. 不适合使用容器查询的场景

容器查询不是万能的,以下场景可能不适合用容器查询:

  • 页面级布局:整体页面布局(比如几栏布局、导航栏、页脚等),还是用媒体查询更合适
  • 简单的组件:非常简单的组件,样式不随容器大小变化,不需要用容器查询
  • 需要兼容老浏览器的项目:如果项目需要兼容老版本浏览器,容器查询的支持还不够好,需要谨慎使用

3. 最佳实践

使用容器查询的一些最佳实践:

  • 从移动优先开始:和媒体查询一样,容器查询也建议从移动优先开始。先写小容器的样式(默认样式),然后用min-width的容器查询,逐步增加大容器的样式
  • 合理设置容器:不要给所有元素都设置容器,只给需要被查询的元素设置容器。设置太多容器,可能会影响性能
  • 用容器查询长度单位做流式排版:配合clamp()和容器查询长度单位,实现流式排版,让组件在不同大小的容器里都有合适的排版
  • 渐进增强:容器查询是新特性,要做好降级方案。对于不支持容器查询的浏览器,提供一个合理的默认样式
  • 测试不同容器大小:开发的时候,要测试组件在不同大小的容器里的表现,确保在各种情况下都有合适的样式

六、浏览器支持和降级方案

容器查询是一个比较新的CSS特性,浏览器支持情况需要关注。

1. 浏览器支持情况

截至2020年9月,容器查询还处于草案阶段,浏览器支持还很有限。

  • Chrome:需要开启实验性标志(chrome://flags/#enable-container-queries)
  • Firefox:需要开启实验性标志(layout.css.container-queries.enabled)
  • Safari:暂不支持

虽然目前浏览器支持还不好,但容器查询是CSS领域最受期待的特性之一,各大浏览器厂商都在积极推进。相信在不久的将来,容器查询会得到广泛支持。

2. 降级方案

在容器查询得到广泛支持之前,我们需要一些降级方案。

方案一:使用Polyfill

有一些容器查询的Polyfill,可以在不支持的浏览器里模拟容器查询的功能。

比较知名的Polyfill有:

  • container-query-polyfill:Google Chrome团队开发的Polyfill
  • cq-prolyfill:另一个容器查询Polyfill

使用Polyfill的好处是,可以直接用标准的容器查询语法,未来浏览器支持了,直接移除Polyfill就行。但Polyfill会增加页面加载时间,也可能有性能问题。

方案二:用JavaScript模拟

可以用JavaScript来模拟容器查询的功能。监听容器的大小变化,根据容器大小给元素添加不同的class,然后用CSS控制样式。

比如:

const container = document.querySelector('.card-container');
const card = document.querySelector('.card');

function updateCardLayout() {
  const width = container.offsetWidth;
  if (width >= 400) {
    card.classList.add('card--horizontal');
    card.classList.remove('card--vertical');
  } else {
    card.classList.add('card--vertical');
    card.classList.remove('card--horizontal');
  }
}

window.addEventListener('resize', updateCardLayout);
updateCardLayout();

这种方式的好处是,兼容性好,所有浏览器都支持。但缺点是,需要写JavaScript代码,维护成本比较高,而且性能不如原生的容器查询。

方案三:用CSS类手动控制

如果组件的使用场景比较固定,可以手动给组件的容器添加不同的class,然后用CSS控制样式。

比如:

<div class="card-container card-container--wide">
  <div class="card">...</div>
</div>

<div class="card-container card-container--narrow">
  <div class="card">...</div>
</div>
.card-container--wide .card {
  display: flex;
  flex-direction: row;
}

.card-container--narrow .card {
  display: flex;
  flex-direction: column;
}

这种方式最简单,兼容性最好,但灵活性最差,需要手动控制容器的class,不能自动适应容器大小。

3. 渐进增强的策略

建议采用渐进增强的策略:

  1. 先写一个基础的样式,在所有浏览器里都能正常显示(比如默认用纵向布局)
  2. 然后用容器查询,在支持的浏览器里,增强样式(比如宽容器用横向布局)
  3. 对于不支持容器查询的浏览器,可以用Polyfill或者JavaScript模拟,提供类似的体验

这样,不管浏览器支不支持容器查询,页面都能正常显示,支持的浏览器还能获得更好的体验。

七、容器查询的未来

容器查询虽然目前浏览器支持还不好,但它的未来是光明的。

1. 标准化进程

容器查询的标准化工作正在积极推进中。CSS Working Group已经把容器查询纳入了标准制定计划,草案在不断完善中。

相信在不久的将来,容器查询会成为正式的CSS标准,得到所有主流浏览器的支持。

2. 更多的容器查询特性

除了基本的容器查询,未来还可能有更多相关的特性:

  • 容器查询样式查询(Style Queries):不仅可以根据容器的大小查询,还可以根据容器的样式(比如CSS变量的值)来查询
  • 容器查询状态查询(State Queries):根据容器的状态(比如是否悬停、是否聚焦)来查询
  • 更强大的容器查询单位:更多的容器相关的长度单位和函数

这些特性,会让容器查询更加强大和灵活。

3. 对前端开发的影响

容器查询的普及,将会对前端开发产生深远的影响:

  • 组件化开发更成熟:容器查询让组件真正做到了"一次编写,到处使用",组件化开发会更加成熟
  • 设计系统更完善:设计系统中的组件,可以更好地适应不同的上下文,设计系统会更加完善
  • 响应式设计进入新阶段:从页面级响应式,进入组件级响应式,响应式设计会更加精细和灵活
  • CSS的能力更强:容器查询让CSS的能力更加强大,很多以前需要JavaScript才能实现的功能,现在用纯CSS就能实现

八、写在最后

CSS容器查询,是CSS领域最受期待的新特性之一。它解决了响应式设计中一个长期存在的痛点,让组件可以根据自身容器的大小来调整样式,真正实现了组件级的响应式设计。

本文从基本概念、基本用法、高级用法、使用场景、浏览器支持、未来发展等方面,详细介绍了CSS容器查询。希望能帮助你了解这个激动人心的新特性。

虽然目前容器查询的浏览器支持还不够好,但它的未来是光明的。作为前端开发者,我们应该提前学习和了解这个特性,为将来的普及做好准备。同时,也可以在项目中,采用渐进增强的策略,小范围地尝试使用容器查询,积累经验。

CSS在不断发展,新的特性不断涌现。作为前端开发者,我们要保持学习的心态,不断学习新的技术和特性,提升自己的能力,跟上技术发展的步伐。

最后,用一句话结束本文:"容器查询,让响应式设计进入了组件时代。"愿每一个前端开发者,都能掌握这个强大的新特性,构建出更灵活、更强大的组件和页面。