CSS容器查询实战

容器查询示意

CSS容器查询是现代CSS中最令人期待的特性之一,它填补了响应式设计的一个重要空白。长期以来,媒体查询让我们能够根据视口宽度调整布局,但这种方法在组件化开发中显得力不从心——组件并不知道它被放置在多大的容器中。容器查询的出现改变了这一切,让组件能够根据自身的容器大小做出响应。作为一个经历过媒体查询痛苦的组件库开发者,我对这项新特性感到无比兴奋。

为什么需要容器查询?

媒体查询的一个根本限制是它只能响应视口大小,而不是元素所在容器的大小。这在现代组件化开发中带来了诸多问题:

组件复用的困境

考虑一个卡片组件,它可能被用于:

  • 主内容区域(宽容器)
  • 侧边栏(窄容器)
  • 弹窗(中等宽度容器)

使用媒体查询,我们只能根据视口宽度调整样式。当视口很宽但卡片在窄侧边栏中时,卡片会显示错误的布局。

/* 传统媒体查询的问题 */
.card {
  display: flex;
  flex-direction: column;
}

@media (min-width: 768px) {
  .card {
    flex-direction: row; /* 侧边栏中的卡片也会变横向 */
  }
}

理想的解决方案

容器查询让组件能够根据其容器的宽度而不是视口宽度来调整样式:

/* 使用容器查询 */
.card-container {
  container-type: inline-size;
}

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

@container (min-width: 400px) {
  .card {
    flex-direction: row; /* 只有容器足够宽时才横向布局 */
  }
}

基础语法

定义容器

首先需要将一个元素定义为容器:

/* 方式1:只查询内联方向尺寸 */
.sidebar {
  container-type: inline-size;
}

/* 方式2:查询两个方向的尺寸 */
.card-grid {
  container-type: size;
}

/* 方式3:同时定义类型和名称 */
.main-content {
  container-type: inline-size;
  container-name: main;
}

/* 简写形式 */
.main-content {
  container: main / inline-size;
}

container-type选项

  • inline-size:只查询内联方向(通常是宽度)的尺寸

    container-type属性

    • inline-size:容器在行内方向上的尺寸,最常用
    • size:容器的宽度和高度都可用于查询
    • normal:默认值,不是查询容器

    命名容器

    /* 命名容器便于精确定位 */
    .sidebar {
      container-name: sidebar;
      container-type: inline-size;
    }
    
    .main-content {
      container-name: main;
      container-type: inline-size;
    }
    
    /* 在特定容器查询中使用 */
    @container sidebar (min-width: 300px) {
      .widget { /* 样式 */ }
    }
    
    @container main (min-width: 600px) {
      .article { /* 样式 */ }
    }

    简写语法

    .container {
      /* 简写形式 */
      container: name / inline-size;
    }
    
    .sidebar {
      container: sidebar / inline-size;
    }
    
    .main {
      container: main / size;
    }

    容器查询语法

    容器查询布局示意

    基本查询

    /* 宽度查询 */
    @container (min-width: 300px) {
      .card { /* 样式 */ }
    }
    
    @container (max-width: 600px) {
      .card { /* 样式 */ }
    }
    
    /* 高度查询(需要container-type: size) */
    @container (min-height: 400px) {
      .card { /* 样式 */ }
    }
    
    /* 方向查询 */
    @container (orientation: landscape) {
      .card { /* 横向样式 */ }
    }
    
    @container (orientation: portrait) {
      .card { /* 纵向样式 */ }
    }

    复合查询

    /* 使用and、or、not */
    @container (min-width: 400px) and (max-width: 800px) {
      .card { /* 中等宽度 */ }
    }
    
    @container (min-width: 600px) or (min-height: 400px) {
      .card { /* 任一条件 */ }
    }
    
    @container not (max-width: 300px) {
      .card { /* 非 narrow */ }
    }

    容器查询单位

    CSS引入了新的容器相对单位:

    • cqw:容器宽度的1%
    • cqh:容器高度的1%
    • cqi:容器内联尺寸的1%
    • cqb:容器块尺寸的1%
    • cqmin:min(cqi, cqb)
    • cqmax:max(cqi, cqb)
    .container {
      container-type: inline-size;
    }
    
    .card {
      /* 字体大小根据容器宽度变化 */
      font-size: clamp(14px, 3cqw, 24px);
      
      /* 间距相对于容器 */
      padding: 2cqw;
      
      /* 圆角相对于容器 */
      border-radius: 1cqw;
    }

    实战模式

    自适应卡片组件

    <!-- HTML -->
    <div class="card-container">
      <article class="card">
        <img src="image.jpg" alt="" class="card-image">
        <div class="card-content">
          <h3 class="card-title">标题</h3>
          <p class="card-text">描述文本</p>
          <button class="card-btn">查看详情</button>
        </div>
      </article>
    </div>
    
    /* CSS */
    .card-container {
      container-type: inline-size;
    }
    
    .card {
      display: grid;
      gap: 1rem;
    }
    
    /* 小容器:垂直布局 */
    @container (max-width: 399px) {
      .card {
        grid-template-rows: auto 1fr;
      }
      
      .card-image {
        aspect-ratio: 16/9;
        object-fit: cover;
      }
      
      .card-title {
        font-size: 1rem;
      }
      
      .card-text {
        display: none; /* 空间不足,隐藏描述 */
      }
    }
    
    /* 中等容器:紧凑横向 */
    @container (min-width: 400px) and (max-width: 599px) {
      .card {
        grid-template-columns: 120px 1fr;
      }
      
      .card-image {
        aspect-ratio: 1;
      }
      
      .card-title {
        font-size: 1.1rem;
      }
      
      .card-text {
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
      }
    }
    
    /* 大容器:宽松横向 */
    @container (min-width: 600px) {
      .card {
        grid-template-columns: 200px 1fr;
      }
      
      .card-image {
        aspect-ratio: 4/3;
      }
      
      .card-title {
        font-size: 1.25rem;
      }
      
      .card-text {
        display: block;
      }
    }

    响应式导航组件

    .nav-container {
      container-type: inline-size;
    }
    
    .nav {
      display: flex;
      align-items: center;
      gap: 1rem;
    }
    
    .nav-links {
      display: none;
    }
    
    @container (min-width: 500px) {
      .nav-links {
        display: flex;
        gap: 1.5rem;
      }
      
      .nav-menu-btn {
        display: none;
      }
    }
    
    @container (min-width: 700px) {
      .nav-links {
        gap: 2rem;
      }
      
      .nav-search {
        display: flex;
      }
    }

    自适应网格布局

    .grid-container {
      container-type: inline-size;
    }
    
    .grid {
      display: grid;
      gap: 1rem;
      grid-template-columns: 1fr;
    }
    
    @container (min-width: 400px) {
      .grid {
        grid-template-columns: repeat(2, 1fr);
      }
    }
    
    @container (min-width: 600px) {
      .grid {
        grid-template-columns: repeat(3, 1fr);
      }
    }
    
    @container (min-width: 900px) {
      .grid {
        grid-template-columns: repeat(4, 1fr);
        gap: 1.5rem;
      }
    }

    组件化设计

    独立组件样式

    /* 组件样式完全独立于页面 */
    /* components/card.css */
    
    .card-container {
      container-type: inline-size;
      container-name: card;
    }
    
    .card {
      /* 基础样式 */
    }
    
    @container card (min-width: 400px) {
      .card {
        /* 变体样式 */
      }
    }
    
    /* 无论在哪里使用,都能正确响应 */
    <aside class="sidebar">
      <div class="card-container">
        <div class="card">...</div>
      </div>
    </aside>
    
    <main class="content">
      <div class="card-container">
        <div class="card">...</div>
      </div>
    </main>

    与CSS变量结合

    .card-container {
      container-type: inline-size;
      
      /* 默认变量值 */
      --card-columns: 1;
      --card-gap: 1rem;
      --card-image-ratio: 16/9;
    }
    
    @container (min-width: 400px) {
      .card-container {
        --card-columns: 2;
        --card-gap: 1.5rem;
        --card-image-ratio: 4/3;
      }
    }
    
    @container (min-width: 600px) {
      .card-container {
        --card-columns: 3;
        --card-gap: 2rem;
        --card-image-ratio: 1;
      }
    }
    
    .card {
      display: grid;
      grid-template-columns: repeat(var(--card-columns), 1fr);
      gap: var(--card-gap);
    }
    
    .card-image {
      aspect-ratio: var(--card-image-ratio);
    }

    降级方案

    使用@supports

    /* 默认样式 */
    .card {
      display: flex;
      flex-direction: column;
    }
    
    /* 媒体查询降级 */
    @media (min-width: 768px) {
      .card {
        flex-direction: row;
      }
    }
    
    /* 容器查询(支持时覆盖) */
    @supports (container-type: inline-size) {
      .card-container {
        container-type: inline-size;
      }
      
      .card {
        flex-direction: column;
      }
      
      @container (min-width: 400px) {
        .card {
          flex-direction: row;
        }
      }
      
      /* 禁用媒体查询的影响 */
      @media (min-width: 768px) {
        .card {
          flex-direction: column;
        }
      }
    }

    渐进增强策略

    /* 策略1:移动优先的默认样式 */
    /* 在容器查询不支持时仍可用 */
    
    /* 策略2:使用PostCSS插件 */
    /* postcss-preset-env 支持容器查询转译 */
    
    /* 策略3:JavaScript polyfill */
    /* https://github.com/GoogleChromeLabs/container-query-polyfill */

    浏览器支持

    容器查询已获得所有主流浏览器的支持:

    • Chrome 105+
    • Safari 16+
    • Firefox 110+
    • Edge 105+

    对于需要支持旧浏览器的项目,建议使用polyfill或保持媒体查询作为降级方案。

    性能考量

    容器查询的性能影响

    • 计算开销:容器查询需要浏览器追踪容器尺寸变化
    • 布局循环:避免容器尺寸依赖内部元素的布局
    • 查询复杂度:过多嵌套的容器查询会增加复杂度

    优化建议

    /* 好:容器有明确尺寸 */
    .sidebar {
      container-type: inline-size;
      width: 300px; /* 或 flex: 0 0 300px */
    }
    
    /* 避免:容器尺寸依赖内容 */
    .auto-container {
      container-type: inline-size;
      /* width: auto - 可能导致布局循环 */
    }
    
    /* 避免过度嵌套 */
    .outer-container {
      container-type: inline-size;
      container-name: outer;
    }
    
    @container outer (min-width: 600px) {
      .inner-container {
        container-type: inline-size;
        container-name: inner;
      }
    }
    
    /* 控制嵌套深度 */
    @container inner (min-width: 300px) {
      /* 避免再嵌套更多容器查询 */
    }

    最佳实践

    1. 为组件设置容器:在组件的包装元素上定义容器
    2. 使用命名容器:避免样式冲突,提高可维护性
    3. 保持查询简单:每个断点只处理必要的样式变化
    4. 结合CSS变量:让布局参数更易于管理
    5. 提供降级方案:确保不支持容器查询时仍可使用
    6. 注意性能:避免过度嵌套和复杂的查询条件
    7. 测试各种场景:确保组件在不同容器中表现正确

    总结

    CSS容器查询是响应式设计的一次重大进化。它让组件真正实现了自包含——组件不再关心它被放置在页面的哪个位置,而是根据自身的容器大小做出响应。这大大提高了组件的可复用性和可维护性。

    虽然容器查询的学习曲线不陡峭,但要在项目中合理运用还需要一定的实践经验。建议从小规模组件开始,逐步积累经验。随着浏览器支持的完善,容器查询将成为组件化开发的标配工具。

    希望这篇实战指南能帮助你掌握容器查询的精髓,在组件化开发中游刃有余。记住,好的组件应该像一个独立的生命体,能够在任何环境中适应并展现最佳状态——这正是容器查询带来的魔力。