过去写响应式布局,思路是「用媒体查询判断屏幕宽度,然后调整组件样式」。问题是:同一个卡片组件,放在侧边栏和放在主区域,宽度完全不同,但媒体查询看到的视口是一样的。
容器查询解决了这个问题。
一、容器查询:组件感知自己的宽度
/* 1. 声明这个元素是「查询容器」 */
.card-wrap {
container-type: inline-size;
container-name: card;
}
/* 2. 查询容器宽度,而不是视口宽度 */
.card { display: flex; gap: 12px; }
@container card (max-width: 420px) {
.card { flex-direction: column; }
.card .thumb { width: 100%; }
}
效果:同一个组件,放在窄侧栏里自动变竖向,放在宽主区域里保持横向,不需要任何 JS,也不需要在父组件里传 isCompact 这类 prop。
浏览器支持已经很好了(Chrome 105+、Safari 16+、Firefox 110+)。
二、Grid 的三种高频模式
2.1 自适应卡片流(不用写媒体查询)
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
一行代码就实现了「每列最小 260px,能塞几列塞几列,剩余空间平均分配」。这是最值得记住的一条。
auto-fill 和 auto-fit 的区别:
auto-fill:列数按容器算,即使没有内容也保留空轨道auto-fit:把空轨道折叠,内容会被拉伸铺满
只有一两个卡片时用 auto-fit,不然卡片会挤在左边。
2.2 圣杯布局
.layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr) 280px;
grid-template-areas: "side main toc";
gap: 28px;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: minmax(0, 1fr);
grid-template-areas: "main" "side" "toc";
}
}
minmax(0, 1fr)里的0很关键。写成1fr时,内容的min-content宽度会把列撑破——这就是「代码块把布局撑爆」的经典原因。
2.3 不用 gap 也能对齐的等高卡片
.row { display: grid; grid-auto-flow: column; gap: 12px; }
.row > * { display: grid; align-content: start; }
Grid 的默认行为就是同一行的子项等高,比 flex 少一层 align-items: stretch 的心智负担。
三、:has() 让父元素感知子元素
以前「父元素根据子元素状态变样式」必须靠 JS,现在:
/* 卡片里有图片时才加内边距 */
.card:has(img) { padding: 0; }
/* 表单里出现错误提示时,输入框变红 */
.field:has(.error) input { border-color: #f85149; }
/* 导航里当前项高亮时,给整个 header 加阴影 */
.header:has(.nav a[aria-current="page"]) { box-shadow: 0 2px 12px rgba(0,0,0,.2); }
/* 列表项被勾选后,文字加删除线 */
li:has(input[type="checkbox"]:checked) { text-decoration: line-through; opacity: .6; }
性能上,:has() 在现代浏览器里已经做了很好的优化。但如果选择器特别复杂又作用在长列表上,还是建议加个 class 更稳。
四、逻辑属性:一次写好,双向可用
margin-left 这类物理属性在 RTL 语言里会出问题。逻辑属性和书写方向绑定:
/* 旧写法 */
.element { margin-left: 12px; padding-right: 8px; }
/* 新写法 */
.element { margin-inline-start: 12px; padding-inline-end: 8px; }
配合 direction: rtl 或 writing-mode: vertical-rl,布局自动镜像,不用写第二套 CSS。
五、几个替代 JS 的小技巧
5.1 粘性表头 + 粘性首列
table { border-collapse: separate; border-spacing: 0; }
thead th { position: sticky; top: 0; z-index: 2; background: var(--surface); }
tbody th { position: sticky; left: 0; z-index: 1; background: var(--surface); }
thead th:first-child { z-index: 3; } /* 左上角交叉处,层级最高 */
5.2 不用 JS 的折叠面板
<details>
<summary>常见问题</summary>
<p>答案在这里。</p>
</details>
details[open] summary { font-weight: 600; }
<details> 自带键盘可达性和无障碍语义,比手写 div + JS 好得多。
5.3 平滑滚动与滚动吸附
html { scroll-behavior: smooth; }
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}
.carousel > * { scroll-snap-align: start; flex: 0 0 80%; }
六、体积的取舍
现在很多框架项目里,CSS 的体积远小于 JS。一个纯 CSS 的响应式方案通常只有几 KB,而对应的 JS 版本要引入 ResizeObserver、状态管理、重新渲染。
判断标准很简单:这个交互需要保存状态吗?
- 只影响样式的显隐/布局 → 纯 CSS(
:has、容器查询、<details>) - 需要保存到服务端或影响业务数据 → JS
小结
三条能立刻用上的规则:
- 组件级响应式用容器查询,别再用视口媒体查询硬套
- 卡片流用
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)) - 父元素感知子元素状态用
:has(),能省掉一大半状态提升
CSS 这几年进步很快,很多以前「必须上 JS」的场景,现在几行 CSS 就够了——而且更无障碍、更省性能。