登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

用 CSS Container Query 让卡片按容器而不是视口响应

来源:17golang原创

时间:2026-10-07 03:50:49 289浏览 收藏

我第一次把同一张“文章推荐卡片”同时放进主栏和侧边栏时,最别扭的不是 CSS 难写,而是页面明明已经很宽,侧边栏里的卡片却仍然只有三百多像素。用 @media (min-width: 1024px) 判断视口后,主栏卡片和侧边栏卡片会一起进入横排模式:前者舒服,后者被挤得标题只剩几字一行。

CSS Container Query 正好解决这个上下文错位。它让卡片根据实际容器的可用宽度切换布局,而不是猜测浏览器窗口有多宽。下面从一张可复用内容卡片出发,用 container-type、container-name 和 @container 完成布局,再补上可访问性、性能和降级处理。

平台资料:https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries、https://web.dev/learn/css/container-queries

为什么媒体查询会让可复用卡片失去上下文

媒体查询回答的是“视口或设备满足什么条件”,容器查询回答的是“这个组件所在的祖先容器满足什么条件”。两者都合理,只是观察范围不同。页面级导航、全局字号和整体栅格仍适合媒体查询;会在主栏、侧边栏、弹窗或嵌入区域之间移动的组件,更适合把布局变化绑定到容器。

视口、页面栏位、卡片容器和容器查询规则的静态边界关系
图1:视口、页面栏位、卡片容器和组件规则的静态边界关系;连线表示包含或作用,不表示执行顺序。

我现在会先问一个简单问题:如果把组件原样拖到页面另一个区域,它还应该保持当前结构吗?如果答案取决于组件得到的空间,而不是设备类型,那么容器查询通常比再加一组视口断点更贴近真实需求。

判断对象更适合的工具典型任务
整个页面或设备视口@media主导航折叠、全局栅格、打印样式
组件所在区域的宽度@container卡片横竖排、工具栏密度、组件字号
组件内部连续尺度容器查询单位内边距、标题字号、媒体比例微调

先让窄容器里的卡片保持自然阅读顺序

我习惯把窄容器样式当成默认值,再用容器查询做增强。这样即使查询条件不匹配,或者旧环境不支持容器查询,卡片仍是一列正常可读的内容,而不是缺少关键布局规则。

团队在白板前讨论组件布局

前端实践

让卡片真正知道自己有多宽

同一个组件可以在主栏横排,在侧边栏保持纵排。

阅读完整教程

这里多包了一层 .story-card-shell。尺寸容器查询通常用于改变查询容器的后代,而不是让容器直接查询自己后再改自己的尺寸。把外层设为查询容器,内层卡片作为被调整的后代,边界会更清楚,也不容易写出互相影响尺寸的规则。

给卡片建立清晰的查询边界

对于横向书写的页面,卡片主要关心可用宽度,所以使用 container-type: inline-size。再给它一个名称 card,后面的查询就不会误匹配页面中其他尺寸容器。

卡片容器声明、查询条件与内部内容结构的静态关系
图2:具名尺寸容器、查询条件与卡片内部结构的静态关系;规则只影响查询容器的后代。
.story-card-shell {
  /* 只查询行内轴尺寸;横向书写时通常对应可用宽度。 */
  container-type: inline-size;

  /* 使用名称限定查询范围,避免命中不相关的祖先容器。 */
  container-name: card;
}

.story-card {
  /* 窄容器默认纵向排列,旧环境也能获得可读布局。 */
  display: grid;
  gap: 1rem;
  padding: clamp(1rem, 4cqi, 1.5rem);
  border: 1px solid #d8dee9;
  border-radius: 1rem;
  background: #fff;
}

.story-card__media {
  /* 清除 figure 的浏览器默认外边距。 */
  margin: 0;
}

.story-card__media img {
  /* 图片跟随媒体区宽度,并保持稳定比例。 */
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 0.75rem;
}

.story-card__body {
  /* 文本列允许在 Grid 中收缩,避免长内容撑破容器。 */
  min-width: 0;
}

@container card (min-width: 38rem) {
  .story-card {
    /* 只有名为 card 的容器足够宽时,才切换为横向结构。 */
    grid-template-columns: minmax(12rem, 0.8fr) minmax(0, 1.2fr);
    align-items: center;
  }

  .story-card__media img {
    /* 横排时提高媒体区纵向占比,避免图片显得过扁。 */
    aspect-ratio: 4 / 3;
  }
}

断点写成 38rem,不是因为它对应某种设备,而是因为卡片在这个宽度附近能同时容纳可用的图片列和正文列。最稳妥的做法是把组件放进可调宽度的演示容器,缩放到标题、摘要和链接都舒服的位置,再记录这个组件断点。

容器单位让变化更连续,但不要把所有尺寸都绑上去

cqi 表示查询容器行内尺寸的百分之一。示例中的 4cqi 会随卡片容器变宽而增加,再由 clamp() 限定在 1rem 到 1.5rem 之间。它适合处理内边距、间距或有限范围的字号变化,但不应取代清晰的布局断点。

.story-card h2 {
  /* 标题只在可读范围内平滑变化,避免宽容器产生夸张字号。 */
  font-size: clamp(1.25rem, 1rem + 1.2cqi, 1.8rem);
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.story-card__eyebrow {
  /* 元信息保持稳定,不与主标题争夺视觉层级。 */
  font-size: 0.875rem;
  color: #52606d;
}

如果没有可用的查询容器,容器查询单位会回退到相应轴的小视口单位。为了避免一个被意外移出容器的组件突然按视口缩放,我仍会给关键值加上下限和上限,并在组件文档里写明外层容器契约。

布局变化不能打乱键盘与读屏顺序

Container Query 只改变视觉布局,不需要复制两套 DOM。图片、标题、摘要和链接的源码顺序在窄卡与宽卡中都保持一致,键盘焦点和读屏顺序也不会因为横竖排切换而跳动。这一点比用 JavaScript 按宽度渲染两套模板更省心。

  • 不要为了“左图右文”随意使用会改变视觉顺序的 order,除非源码顺序本身仍然合理。
  • 卡片里只有一个主要链接时,给它明确文字;不要把整张卡做成多层嵌套链接。
  • 图片提供与内容有关的 alt;纯装饰图应使用空的 alt=""。
  • 标题很长时使用 min-width: 0 和断词规则,不能靠隐藏溢出来掩盖问题。

性能上更重要的是控制边界数量

声明尺寸查询容器会引入相应的尺寸约束,浏览器借此避免后代样式改变祖先尺寸后反复触发条件翻转。对普通卡片而言,这通常不是性能问题;真正需要克制的是把页面上每一层包装元素都声明成容器,导致规则来源难以追踪。

我的取舍是:只在组件可能被不同区域复用、而且内部确实需要响应容器时声明容器;页面大框架继续用媒体查询;组件内连续的小尺寸调整才使用容器单位。这样 DevTools 中看到的边界少,团队也更容易知道一条 @container 到底依赖哪个祖先。

为不支持环境保留可用的窄卡样式

容器查询很适合渐进增强。默认纵排样式已经可以完成阅读任务,支持环境再启用具名容器与横排布局。不要把正文显示、主要链接或表单提交能力放到容器查询内部。

/* 默认样式已经是可用的纵向卡片。 */
.story-card {
  display: grid;
  grid-template-columns: 1fr;
}

@supports (container-type: inline-size) {
  .story-card-shell {
    /* 支持容器查询时才建立尺寸查询上下文。 */
    container: card / inline-size;
  }

  @container card (min-width: 38rem) {
    .story-card {
      /* 增强为横排,不影响不支持环境的基础阅读。 */
      grid-template-columns: minmax(12rem, 0.8fr) minmax(0, 1.2fr);
    }
  }
}

如果项目必须覆盖更旧的浏览器,可以给某些固定布局保留一条媒体查询,但要把它当作近似降级,而不是复制整套容器规则。JavaScript ResizeObserver 更适合驱动图表重算、Canvas 尺寸或无法仅靠 CSS 表达的行为;单纯切换卡片 CSS 布局,没有必要额外维护监听器和 class 状态。

几个容易踩到的边界状态

  • 查询容器自己没有变化:@container 规则作用于后代。需要改变外框时,再包一层最清晰。
  • 规则命中了错误祖先:匿名查询会寻找最近的合格祖先;复杂页面优先使用 container-name。
  • 卡片仍被长文本撑开:给 Grid 或 Flex 子项设置 min-width: 0,再处理 URL、代码和连续字符断行。
  • 断点数量越来越多:断点应对应结构变化,不要为每十几个像素写一条规则;连续尺寸交给 clamp() 和容器单位。
  • 嵌套组件互相干扰:每个组件使用清晰的容器名称,并让规则只描述自己的后代。

适合把哪些组件改成 Container Query

在我实际改造过的组件里,收益最大的通常不是整页,而是卡片、工具栏、筛选面板、评论项和数据摘要块。这些组件会被放进不同宽度的区域,且布局变化由局部空间决定。反过来,如果一个模块始终占满页面,或者变化确实取决于设备与视口,媒体查询依然更直接。

最终判断标准很朴素:页面在问“屏幕有多宽”,组件在问“我现在有多宽”。先把窄布局写成可靠默认值,再给外层建立具名 inline-size 容器,最后只在真正需要结构变化的位置写 @container。这样卡片离开主栏、进入侧边栏或嵌入新页面时,通常不需要再为宿主页面补一组特例。

相关问题

Container Query 可以完全替代媒体查询吗?

不能,也没必要。页面级导航、整体栅格、打印和设备能力仍适合媒体查询;容器查询主要解决可复用组件对局部空间的响应。

为什么更常用 inline-size 而不是 size?

多数卡片只需根据行内轴宽度变化。inline-size 只建立行内尺寸查询上下文,意图更准确;只有确实要同时查询行内轴和块轴尺寸时,才考虑 size。

容器查询断点应该沿用 768px、1024px 吗?

不建议机械沿用设备断点。把组件放进可调宽度容器,以内容开始拥挤或结构可以自然展开的位置作为断点,更符合组件自身需求。

什么时候仍然需要 ResizeObserver?

当尺寸变化要驱动 JavaScript 行为,例如重算图表、更新 Canvas 或同步非 CSS 状态时再用。只是改变 CSS 排列、间距或字号,优先使用容器查询。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>