CSS :has() 与容器查询实战:不写 JS 的组件响应式
媒体查询看的是视口宽度,不是组件所在容器的宽度。同一个卡片组件放在侧栏和主区域,需要两种布局,但视口宽度是一样的——媒体查询没法区分。:has() 让选择器能根据子元素状态选父元素,容器查询让组件能根据自身容器宽度调整布局。两者合起来,能做到以前必须写 JS 才能实现的响应式效果。
一、:has() 实战
1. 表单状态联动
以前要给父容器加高亮,得监听 checkbox 的 change 事件再切 class。现在一行 CSS 搞定:
.card:has(input[type="checkbox"]:checked) {
border-color: #3b82f6;
background: #eff6ff;
}
/* 有错误提示时,输入框变红 */
.field:has(.error) input {
border-color: #ef4444;
}
2. 有无图片的卡片自适应
/* 有图片:横向布局 */
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
}
/* 无图片:纯文本,多留点内边距 */
.card:not(:has(img)) {
padding: 1.5rem;
}
3. 选前面的兄弟元素
组合器只能选后面的兄弟,:has(+ selector) 可以反向选:
/* 标题后面紧跟表格时,收紧下边距 */
h2:has(+ table) {
margin-bottom: 0.5rem;
}
二、容器查询实战
先声明容器,告诉浏览器这个元素要作为查询基准:
.card-grid {
container-type: inline-size;
container-name: card;
}
再用 @container 按容器宽度设置断点:
@container card (max-width: 400px) {
.card { grid-template-columns: 1fr; }
}
@container card (min-width: 600px) {
.card { grid-template-columns: 200px 1fr; }
}
同一个组件放进 320px 的侧栏就堆叠,放进 800px 的主区域就并排,不需要组件知道自己被放在哪。
三、组合用法
容器宽度决定断点,:has() 决定内容形态,两个判断互相独立:
.card-wrap { container-type: inline-size; }
@container (max-width: 320px) {
.card h3 { font-size: 1rem; }
.card .meta { display: none; }
}
@container (min-width: 480px) {
.card { display: grid; grid-template-columns: 160px 1fr; }
.card:not(:has(img)) { grid-template-columns: 1fr; }
}
有图的卡片横向排,无图的单列;窄容器里隐藏次要信息。全部由 CSS 判断,React/Vue 组件里一行状态逻辑都不用加。
四、兼容性与降级
:has():Chrome 105+、Safari 15.4+、Firefox 121+@container:Chrome 105+、Safari 16+、Firefox 110+
用 @supports 包一层,老浏览器走默认布局,不会白屏:
@supports selector(:has(*)) {
.card:has(img) { /* 增强布局 */ }
}
五、什么时候还得写 JS
三种情况:跨组件的状态依赖、需要读取真实像素值参与计算、和 Canvas 或图表库联动。单纯的布局开关交给 CSS 更省事,SSR 时也不会先闪一下错误布局。
小结
媒体查询回答「设备有多宽」,容器查询回答「我所在的容器有多宽」,:has() 回答「我的子元素处于什么状态」。三个维度互补,覆盖了大多数以前要用 ResizeObserver 或事件监听才能做的响应式需求。
- 点赞
- 收藏
- 关注作者
评论(0)