首页 / 文章

容器查询与媒体查询:页面决策与组件决策

通过卡片、控制面板和单元示例,了解何时该使用媒体查询,何时容器查询更为合适,从而让可复用的组件能够灵活适应不同场景。

发布者:REACTAPP.TOP阅读约 16 分钟3602 词

来源: Container Queries vs Media Queries: How Modern CSS Changes Responsive Design

大多数响应式 CSS 仍然依赖于一个参数:浏览器窗口的宽度。这对整个页面来说效果不错,但对于可复用组件而言却存在问题——在侧边栏中显示的卡片与在宽主列中显示的相同卡片虽然占据的空间差异很大,但看到的视口却完全一致。本指南将介绍容器查询如何弥补这一缺陷、媒体查询为何仍是更佳选择,以及如何将两者与流体单位结合,构建出一个可合理设计的响应式系统。

典型的以视口为驱动的规则如下:一旦窗口宽度低于某个断点,卡片就会将其内容垂直堆叠。

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

这里没有任何问题。缺陷在于设计理念:规则只学习了窗口的宽度,却从未学习过卡片本身的宽度。在小型演示中这些数值会保持一致;但在充满网格、侧边栏和面板的应用程序中,它们就会出现偏差,而正是这种差异导致了布局错误。容器查询让组件能够询问“我实际有多少可用空间?”,而非“屏幕有多宽?”

为何视口断点会误导可复用卡片的设计

以一个常见的产品卡片为例:一张图片旁边是一段文字,包含标题、简短描述以及操作提示。这里的标记被分成了两部分,但实际上它属于同一个元素。

<article class="product-card">
  <img src="product.jpg" alt="Product">
  <div class="product-info">
    <h2>Wireless Headphones</h2>
    <p>Noise cancellation with 30-hour battery life.</p>
    <button>Buy now</button>
  </div>
</article>

最初的样式处理通常会使用flexbox将图片和文字并排显示,然后在小屏幕上通过视口断点让它们堆叠在一起。

.product-card {
  display: flex;
  gap: 1rem;
}
@media (max-width: 768px) {
  .product-card {
    flex-direction: column;
  }
}

在卡片未被重复使用的情况下,这种方式似乎没什么问题。在宽度较大的桌面页面上,有足够的水平空间,侧边并排布局看起来很合适。

Desktop page
┌───────────────────────────────────────────┐
│                                           │
│   Product Card                            │
│   ┌──────────┐  ┌─────────────────────┐   │
│   │  Image   │  │ Product information │   │
│   └──────────┘  └─────────────────────┘   │
│                                           │
└───────────────────────────────────────────┘

将同样的卡片放入狭窄的侧边栏中,即便窗口大小仍为桌面尺寸,也需要采用堆叠布局。

┌───────────────┐
│               │
│   Sidebar     │
│               │
│ ┌───────────┐ │
│ │   Image   │ │
│ ├───────────┤ │
│ │   Info    │ │
│ └───────────┘ │
│               │
└───────────────┘

在控制面板的一行中放置多个此类卡片,每个卡片的宽度会根据其所在列的不同而有所差异。

┌───────────────────────────────────────────┐
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │   Card      │ │       Card           │ │
│ └─────────────┘ └──────────────────────┘ │
└───────────────────────────────────────────┘

三种放置方式,三种不同的宽度,但视口尺寸却保持不变。基于窗口大小的规则无法区分它们,因此侧边栏中的卡片依然显得拥挤。

媒体查询究竟擅长什么

媒体查询用于评估页面显示环境中的各种属性:视口尺寸、方向、颜色方案、指针类型、减少动画的偏好设置等等。宽度查询就是最常见的例子。

@media (max-width: 768px) {
  ...
}

信息流程很简单:浏览器测量视口,然后判断查询是否匹配,最后布局会根据结果作出相应调整。

Viewport
    ↓
Media Query
    ↓
Layout changes

正因如此,媒体查询才是处理真正属于整个页面的布局决策的合适工具。一个典型的例子是带有固定宽度侧边栏的双列界面:

.page-layout {
  display: grid;
  grid-template-columns: 1fr 300px;
}

当窗口宽度低于某个阈值时,该界面会缩减为单列:

@media (max-width: 900px) {
  .page-layout {
    grid-template-columns: 1fr;
  }
}

这正是媒体查询的合理应用:页面是否有空间显示侧边栏确实属于视口相关的问题。而当同一机制被用来决定单个组件的内部布局时,问题就出现了。

容器查询的工作原理

容器查询允许某个元素响应被明确标记为查询容器的祖先元素的尺寸变化。设置它需要两个步骤。首先,声明该容器:

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

inline-size值告知浏览器,后代元素可以查询该容器的内联尺寸,即在水平书写模式下的宽度。其次,为该容器编写规则:

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

此时信息流来自容器而非窗口:

Container
    ↓
Container Query
    ↓
Component layout

该卡片不再关心访问者使用的是手机还是大显示器;它会根据父元素为其分配的空间作出响应。

常让人困惑的一个细节

容器查询用于设置容器的后代元素的样式,而非容器本身。正因如此,示例中总是将包装层(如.card-container、.card-wrapper)与内部元素(如.product-card)搭配使用。如果在.product-card上设置container-type,再通过查询来改变该元素的flex-direction,则不会产生任何效果。请将包装层与组件视为独立的元素。

第二个细节:inline-size限制意味着容器的宽度不能再由其内容决定。作为普通流布局中的块级子元素的容器没有问题,因为其宽度来自父元素。而那些依赖自适应尺寸的容器,比如内联块元素或没有明确宽度值的绝对定位元素,在应用了限制后宽度可能会变为零。

视口宽度与组件宽度

在主列较宽、侧列较窄的仪表板中,这种差异就显得尤为明显:

.dashboard {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 1rem;
}

假设窗口宽度为1440像素。针对卡片设定的基于视口的规则如下:

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

在这样宽的窗口上,该规则永远不会匹配。然而位于较窄侧列中的卡片宽度可能只有约350像素。媒体查询只能获取一个数值:

Viewport = 1440px

该卡片与其他卡片一起存在:

Component width = 350px

只有第二个数值能反映卡片的真实状态,且只有容器查询才能对此作出响应。

最简的容器查询配置

实现方式很简单。将包装元素标记为容器:

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

当卡片的容器宽度至少为600px时,为其设置两列网格布局:

@container (min-width: 600px) {
  .product-card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

在此宽度以下则回退到简单的块级布局:

@container (max-width: 599px) {
  .product-card {
    display: block;
  }
}

标记只需在卡片周围添加包装元素即可:

<div class="card-wrapper">
  <article class="product-card">
    ...
  </article>
</div>

现在卡片的布局由其容器决定,因此将包装元素移到另一列中就能自动形成正确的布局。需要注意的是,这两条规则分别使用了min-width: 600px和max-width: 599px;当宽度为小数时,容器可以处于这两个值之间。而新的范围语法@container (width >= 600px)和@container (width < 600px)则避免了这种间隙,在新代码中更值得优先使用。

为何这符合基于组件的架构

容器查询比一般的新的 CSS 功能更为重要,因为现代前端是由可重用的组件构成的,这些组件可能是 React、Vue 或 Angular 中的框架组件,也可能是无需框架的 Web Components,或是共享设计系统中的基础构件。它们都不应需要知道自己最终会出现在哪个页面区域。以 user-card 元素为例,它可能会渲染在主要内容区域:

<main>
  <user-card></user-card>
</main>

它也可能会渲染在侧边栏中:

<aside>
  <user-card></user-card>
</aside>

或者它可能会被重复显示在控制面板网格中:

<div class="dashboard-grid">
  <user-card></user-card>
  <user-card></user-card>
  <user-card></user-card>
</div>

将视口断点硬编码到该组件中,实际上是一种无法验证的假设:

@media (max-width: 768px) {
  ...
}

数字768并不能反映组件的实际使用场景。容器查询提供了更清晰的规范:在给定如此大的空间时,应采用这种布局方式。如果你的组件是独立于框架的构建模块,那么关于在可复用UI架构中框架边界应位于何处的讨论,可以作为这一理念的有益补充。

让个人资料卡片适应周围环境

这是一个简洁的个人资料卡片,将头像置于姓名和职位旁边:

.profile-card {
  display: flex;
  gap: 1rem;
  align-items: center;
}

它的包装容器就变成了查询容器:

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

当该容器的宽度为400像素或更小时,卡片会将其内容堆叠并居中显示:

@container (max-width: 400px) {
  .profile-card {
    flex-direction: column;
    text-align: center;
  }
}

在空间较大的容器中,头像会显示在文本旁边:

┌──────────────────────────────────────┐
│ [Avatar]  John Smith                 │
│           Senior Developer           │
└──────────────────────────────────────┘

在空间紧张的情况下,同一组件会自动垂直重新排列:

┌──────────────────┐
│                  │
│     [Avatar]     │
│                  │
│    John Smith    │
│ Senior Developer │
│                  │
└──────────────────┘

没有尺寸调整监听器,没有第二种“紧凑”版本,也没有由父页面控制的类。

媒体查询仍有用武之地

容器查询并不能替代媒体查询,它们解决的是不同的问题。以下是常见决策的快速分类:

  • 折叠主导航栏:媒体查询。
  • 隐藏页面级的侧边栏:媒体查询。
  • 在横向与纵向布局之间切换卡片:容器查询。
  • 让可复用组件适配其所在位置:容器查询。
  • 调整整个网站的字体样式:媒体查询。
  • 让控制面板组件根据列宽做出响应:容器查询。
  • 将整个应用界面切换为不同布局:媒体查询。
  • 在受限的父容器中改变组件的行为:容器查询。
  • 思维模型:全局决策与组件决策

    最清晰的划分方式是将页面需要决定的内容与每个组件需要决定的内容分开。

    媒体查询关注视口情况

    媒体查询背后的问题是:

    "What is the viewport doing?"
    

    它们适用于:

    • 页面结构
    • 导航设计
    • 全局间距设置
    • 全局字体样式
    • 重大布局调整

    容器查询关注组件的周围环境

    容器查询背后的问题是:

    "What is my component's environment doing?"
    

    它们适用于:

    • 卡片组件
    • 小部件
    • 面板
    • 可复用的UI组件
    • 控制台模块
    • 设计系统中的组件

    使用容器查询单位进行尺寸设置

    容器查询不仅仅是条件块。CSS还定义了相对于最近查询容器的长度单位,这样数值就可以连续变化,而不会在某个断点处发生跳跃:

    .title {
      font-size: 5cqi;
    }
    

    此处cqi表示容器内联尺寸的1%,因此5cqi即其宽度的5%。所有可用单位如下:

    cqw  → container width
    cqh  → container height
    cqi  → container inline size
    cqb  → container block size
    cqmin → smaller container dimension
    cqmax → larger container dimension
    

    一种常见的做法是将容器单位包裹在clamp()函数中,这样数值会随容器大小变化,但始终保持在合理的范围内:

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

    现在标题会根据组件的空间而非窗口大小进行缩放,而像 vw 这样的视口单位无法表达这种缩放关系。有两个需要注意的点:如果某个元素没有父级查询容器,这些单位就会退化为较小的视口单位,因此虽然仍会生成数值,但并非你期望的结果。另外,如上例在 clamp() 中设置 rem 的最小值和最大值对无障碍性很重要:完全由容器宽度决定的字体大小无法响应用户的文本大小设置。

    在大型应用中为容器命名

    对于小型页面,使用匿名容器就足够了:

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

    当容器嵌套时,单纯的 @container 规则会匹配最近的父级容器,而该容器可能并非您所期望的。使用 container 缩写可以让您为容器指定名称及其类型:

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

    这样就可以直接针对该容器应用规则了:

    @container dashboard (min-width: 700px) {
      .widget {
        grid-template-columns: 1fr 1fr;
      }
    }
    

    比较无名称的条件:

    @container (min-width: 700px)
    

    与有名称的条件相比:

    @container dashboard (min-width: 700px)
    

    有名称的版本能够明确表达其意图,即便后来有人在控制面板和组件之间添加了其他容器,它依然可以正常工作。

    谨慎选择容器边界

    容器查询功能非常强大,但这并不意味着就要在每个元素上都添加此类声明:

    container-type: inline-size;
    

    每个容器都会应用布局、样式以及内联尺寸限制,这些因素会改变元素的尺寸方式,因此容器应当用于标记有意义的组件边界,而非随意放置。以一个包含多个小部件的仪表板为例:

    <section class="dashboard">
      <article class="widget">
        <div class="widget-content">
          ...
        </div>
      </article>
    </section>
    

    合理的做法是将该仪表板设为命名容器:

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

    让每个小部件对其作出响应。规则并非“处处使用容器查询”,而是“在行为取决于可用空间的情况下使用它们”。

    用基于内容的断点替代传统断点堆叠

    许多样式表都包含类似这样的视口断点层级结构:

    @media (max-width: 1200px) {
      ...
    }
    
    @media (max-width: 1024px) {
      ...
    }@media (max-width: 900px) {
      ...
    }@media (max-width: 768px) {
      ...
    }@media (max-width: 640px) {
      ...
    }@media (max-width: 480px) {
      ...
    }
    

    有时这种布局是合理的。但往往它表明组件是根据设备宽度而非自身内容来设计的。容器查询则引导我们思考不同的问题:不要问“团队通常使用哪个断点?”,而应问“在什么宽度下该组件的显示效果会变差?”。这样的答案能帮助我们确定以内容为导向的断点:

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

    420px这个数值并不对应某款特定的手机,只是该卡片内容在此宽度处达到边界而已。以这种方式确定的断点能够适应布局变化和新型设备的出现。

    设计不依赖定位的组件

    容器查询还能促使团队打造真正独立的组件,使其在任何渲染位置的行为都具有可预测性。以设计系统中的按钮组为例:它可能位于页面的表单内部:

    Page
     └── Form
          └── Button Group
    

    在仪表板上的卡片内部:

    Dashboard
     └── Card
          └── Button Group
    

    或在侧边栏中的卡片内部:

    Sidebar
     └── Card
          └── Button Group
    

    这三种情况下的视口是相同的,但空间却不同。使用容器查询后,该组件无需了解其上层的结构即可自动调整。

    可复用的响应式卡片模板

    这里有一个可直接采用的完整模板。标记会将卡片包裹在容器元素中;同样,HTML代码会分两段展示。

    <div class="card-container">
      <article class="card">
        <img src="image.jpg" alt="">
    
        <div class="card-content">
          <h2>Performance Dashboard</h2>
          <p>
            Monitor application performance and infrastructure metrics.
          </p>
          <a href="#">View dashboard</a>
        </div>
      </article>
    </div>
    

    该包装元素被定义为容器:

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

    该卡片最初为单列网格,显示16:9比例的图片。当其容器宽度达到600像素时,会切换为在内容旁固定显示图片的布局,并将图片调整为正方形。这些规则之间没有换行符,虽然这是有效的CSS写法,但在将其粘贴到代码库中时仍建议重新格式化。

    .card {
      display: grid;
      gap: 1rem;
    }.card img {
      width: 100%;
      aspect-ratio: 16 / 9;
      object-fit: cover;
    }@container (min-width: 600px) {
      .card {
        grid-template-columns: 200px 1fr;
        align-items: center;
      }  .card img {
        aspect-ratio: 1;
      }
    }
    

    注意这个样式表中缺少了什么:

    @media (max-width: 768px)
    

    该卡片从不参考视口信息,其所有决策都基于容器尺寸。

    将容器查询与自适应网格结合使用

    容器查询尤其适合与能够自行决定列数的网格系统搭配使用:

    .dashboard {
      display: grid;
      grid-template-columns:
        repeat(auto-fit, minmax(280px, 1fr));
      gap: 1rem;
    }
    

    使用 repeat(auto-fit, minmax(280px, 1fr)) 后,浏览器会根据行宽容纳尽可能多的至少280px宽的列,并拉伸这些列以填满剩余空间。这样每个组件都会有自己的容器:

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

    当其所在区域足够宽时,会切换为双列内部布局:

    @container (min-width: 500px) {
      .widget {
        grid-template-columns: 1fr 1fr;
      }
    }
    

    该控制面板无需视口断点即可适应各种宽度:网格负责决定每个组件所占的空间,而每个组件则自行决定如何使用这些空间。

    响应式布局也与易用性相关

    能够显示在屏幕上并不等同于易于使用。即使一组按钮在技术上可以容纳在狭窄的卡片中,它们仍可能显得过于拥挤,导致点击时容易出错。当卡片较窄时,容器查询可以将这些按钮堆叠起来并拉伸至全宽(相关规则分布在两个代码片段中):

    @container (max-width: 350px) {
      .actions {
        flex-direction: column;
      }
    
      .actions button {
        width: 100%;
      }
    }
    

    全宽堆叠式按钮能提供更大的触摸区域,从而减少误触。文本输入也可遵循相同逻辑,随着组件宽度的变化在限定范围内进行缩放:

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

    具体数值的重要性不如其背后的原则:响应式行为应反映组件实际使用时的环境,而非基于窗口尺寸的猜测。

    何时媒体查询仍是最佳选择

    容器查询并不总是更优的选择。全局导航显然适合使用媒体查询,因为隐藏完整菜单并显示菜单按钮属于页面结构层面的决策(此规则也被分为两条):

    @media (max-width: 900px) {
      .navigation {
        display: none;
      }
    
      .menu-button {
        display: block;
      }
    }
    

    用户偏好是另一类完全不同的内容。对于那些向操作系统请求减少动画效果的用户,降低动画速度与任何容器无关:

    @media (prefers-reduced-motion: reduce) {
      * {
        animation-duration: 0.01ms;
        animation-iteration-count: 1;
      }
    }
    

    那属于环境偏好设置,只有媒体查询才能读取。其目的并非取消媒体查询,而是避免将其用于组件决策。

    响应式 CSS 的三层方法

    对于基于组件的应用,将响应式样式设计分为三层会更有帮助。

    第 1 层:使用媒体查询实现全局响应式

    媒体查询负责处理涉及整个页面的样式问题:

    Page layout
    Navigation
    Global typography
    Major structural changes
    Application shell
    

    第 2 层:使用容器查询实现组件响应式

    容器查询负责处理独立组件的内部布局:

    Cards
    Widgets
    Panels
    Reusable components
    Design-system components
    Dashboard modules
    

    第三层:借助函数与单位实现流畅的响应式效果

    在不需要固定断点的情况下,可以利用现代函数和单位让数值实现连续变化:

    clamp()
    min()
    max()
    minmax()
    cqi
    cqw
    

    例如,标题的大小可以在其容器范围内,在两个固定极限之间动态调整:

    .heading {
      font-size: clamp(1.5rem, 3cqi, 3rem);
    }
    

    这些层次共同构成一个完整的系统,而非零散的断点修补方案。

    真正的变化在于决策权的归属

    人们很容易将容器查询简化为“元素的媒体查询”。从技术上讲这没错,但却忽略了核心要点。实际上变化的是责任分配模式。在传统的响应式CSS中,页面实际上决定了每个组件的行为方式:

    Viewport
       ↓
    Media Query
       ↓
    Component
    

    而使用容器查询后,每个组件都会根据自身的容器来决定行为:

    Component Container
            ↓
    Container Query
            ↓
    Component
    

    那些拥有自身响应式规则的组件更易于重复使用、独立移动、单独测试以及维护。这种所有权的转变正是其持久的价值所在。

    解决每个问题都需要问的一个问题

    当不确定该使用哪种工具时,先思考布局决策是针对页面还是针对组件。如果与页面相关,则使用媒体查询;如果与组件的可用空间相关,则使用容器查询。以下是常见场景的应用示例:

    Should the sidebar disappear?
            ↓
        Media Query
    
    Should this card switch from horizontal
    to vertical?
            ↓
      Container Query
    
    Should the navigation collapse?
            ↓
        Media Query
    
    Should this reusable widget change
    its internal layout?
            ↓
      Container Query
    

    核心要点

    • 媒体查询用于描述视口和用户环境,因此适用于页面结构、导航、全局字体设置以及如减少动画效果之类的偏好设置。
    • 容器查询用于描述组件所占据的空间,因此适用于卡片、小部件、面板以及设计系统中的各类组件。
  • 容器查询用于为容器的后代元素设置样式;应将容器与组件视为独立的元素,并注意那些需要根据内容确定宽度的容器。
  • 基于内容的断点决定了组件实际停止正常工作的位置,其作用时间比基于设备的断点更长。
  • 诸如 cqi 和 cqw 这样的容器单位,配合 clamp() 函数及 rem 单位,能够在不牺牲用户文本大小设置的情况下实现灵活的尺寸控制。
  • 在元素嵌套后为其命名,并且只在有意义的组件边界处创建容器。
  • 最稳健的方案是同时使用媒体查询、容器查询和流体 CSS,而非仅选择其中一种。