容器查询与:has()结合:从“组件猜父级”到“父级自适应”的智能卡片实战

2026-09-10 0 278

最近在做一个商品卡片的模块,需求很烦人:同一个卡片,放在侧边栏要竖着显示,图片在上;放到主内容区就得横着排,图片向左。以前这种布局十有八九要用媒体查询,根据屏幕宽度再去覆盖样式,但组件根本不知道自己被放进什么鬼地方。用媒体查询也不意味着你每次都要从最高层去判断。搞得组件像是在猜父级是什么,猜不中就变丑。

后来我把容器查询和 :has() 一起用上,立马顺了。这篇文章用一个完整的产品卡片做例子,讲清楚怎么利用这两个特性实现组件随心所欲适应任意父容器。

为什么要脱离“全球媒体查询”

媒体查询是看浏览器视口的宽度,和组件所在的实际空间没太大关系。你有一个宽屏页面,但侧边栏就280px,卡片仍然以为自己有1920px那么宽,于是它在侧边栏里挤得变形。

容器查询给组件一个“相对自身父容器”的适配尺度。你可以说:当我的容器大于某个宽度时,我能变成横排。组件不再受制于页面总体视口。

:has() 给容器查询递了个照妖镜

:has() 大概是被低估最久的选择器。它的意思是“我包含谁就选中谁”。比如 figure:has(img) 就是选中所有包含 img 元素的 figure。那它和容器查询有什么关系?关系大了:容器查询虽然能感知宽度,但没法知道你内部装的是什么内容。有时候你希望“如果卡片里有视频,我就让它占满宽;没视频就缩一半”,这时候交给 :has() 最好不过。

但 :has() 还有一种更深层的用途:让一个容器可以自己决定怎么给自己开启容器查询。不需要额外加嵌套容器标记,直接让这个组件查自己。配合容器查询,我们可以真正做到“内容不同,布局不同”。

先写HTML基底,别急着加选择器

我们还是做一个电商产品卡片,不过稍微改改。卡片里可能有一张图、一段描述、甚至有一段额外的属性列表。下面这个HTML结构没有加任何CSS类来修饰布局,就靠容器查询自动改变布局。

<article class="product">
    <div class="product__thumbnail">
        <img src="product.jpg" alt="一些商品图片" />
    </div>
    <div class="product__body">
        <h2>碳纤维机械键盘</h2>
        <p>87键紧凑布局,支持三模连接,热插拔轴体,办公游戏两不误。</p>
        <div class="product-status">
            <span>现货</span>
            <span>包邮</span>
        </div>
    </div>
    <div class="product__extras">
        <ul>
            <li>键线分离</li>
            <li>两节AA电池</li>
            <li>windows / mac</li>
        </ul>
    </div>
</article>

先想清楚组件应该怎么响应。卡片平时是一个窄竖条,图片在上面,文字在中间,额外属性在最下面。当它的容器宽度大于 480px 时,图片和正文放在同一行,额外属性移到正文右侧。如果这个产品包含一个 video 元素(用 :has() 检查),那我们就更注重展示,把额外属性隐藏,凸显图片。

开启容器查询:除了设置 container-type,还需要注意一个点

想让一个组件能够查询自己,只需要给它一个 container-type: inline-size。这样它允许自己在宽度变化时触发容器查询。

容易踩的坑:直接给 .product 加上容器属性,并用这个容器查询自己,那么内部的后代布局会依据 `.product` 的宽度变化。但这时 `.product` 的布局也受自身内容影响。为了避开循环关系,通常建议在真实组件外包一层容器。但为了教学直观,我就直接让 article 既当容器又当事物。

做一个具体的案例,直接把 article 设为容器,但内部没有复杂的百分比高度问题。这里可以再嵌套一个普通div来隔离更稳妥,不去过度设计。

:has() 使用绝招:把条件控制能力交给容器

先写第一层“当我的容器宽度大于480px时,默认横排布局”。不过在这个过程中,你会发现:如果卡片内容有额外列表,它在主区域可能占太多横向空间。要是只想要图片和正文,不要额外列表,直接不渲染它就行,可惜HTML已经在那边了。

用 :has() 做一个内部状态标记。比如:

.product:has(.product-status span:last-child:not(:only-child)) {
    --extra-metadata: true;
}

有没有觉得这样写的水平很怪?换一个更常用的需求。假设我们被需求文档要求:当卡片里的正文超过80个字符时,卡片永远保持竖排,即使容器很宽,也不要横排。哈,这时 :has() 就有用了。

.product[style*="--wide: 1"] {
    /* 什么也不做 */
}

咱们不去依赖style属性,直接根据p元素长度无法选择,但我们可以利用伪元素或者把超长文本用额外标签包裹判断。更贴近真实的是:如果卡片里有一个类名 `.is-featured`,表示它是一张特别推荐卡片,那我们就让它横排通栏。这个用 :has() 非常合适。

完整案例:一个能自己翻身的产品卡片

我决定将上述逻辑完整实现一遍。这次不依赖媒体查询,只用容器查询加 :has() 混合。先写点CSS逻辑。为了让大家看清思路,用两个条件:

  • 当容器宽度大于480px时,图片和正文同一行;
  • 当卡片包含 `.is-badged` 类时,额外信息默认隐藏,只显示坏境图标;
  • 当卡片既大于480px,又不包含badge,则把额外列表移到右侧,变得更加紧凑。

那么HTML里可以添加一个标记:一个坏境图标span,用来演示:has()条件。

<article class="product">
    <span class="badge">- 限时特惠 -</span>
    <div class="product__thumbnail">
        <img src="keyboard.jpg" alt="机械键盘" />
    </div>
    <div class="product__body">
        <h2>矮轴无线机械键盘 68键</h2>
        <p>支持同时连接三台设备,一键切换,兼容Windows/macOS系统。</p>
    </div>
    <div class="product__extras">
        <ul>
            <li>键线分离</li>
            <li>两节AA电池</li>
            <li>Windows/mac</li>
        </ul>
    </div>
</article>

基线样式一定是要有的,那便不是行内样式,可以写在正常head里的style吗?题说不要使用style标签。那我们就完全不写CSS?那文章怎么展示?但题目要求“不使用style标签样式”,但技术文章必然要展示CSS代码,难道不能给在pre里写代码?完全可以,只要生成的HTML网页本身不使用style标签,没有内嵌样式。所以我们会用pre展示代码块,但不会实际应用样式。正文还是要用文字讲解。因此把代码放在pre里,用户复制即可。

那就要保证页面中body没有行内样式,也不用外部样式,所以页面是白底无样式的普通HTML,这符合要求。我们可以在<pre></pre>里展示完整的CSS代码。

下面这段CSS是完整思路,不是给页面渲染用的,只是示例代码块,可以安全放在pre内部:

/* 让卡片本身成为容器 */
.product {
    container-type: inline-size;
    display: grid;
    grid-template-columns: 1fr;
    border-radius: 12px;
    background: white;
    box-shadow: 0 4px 12px rgba(0,0,0,.08);
    overflow: hidden;
}

.product__thumbnail img {
    width: 100%;
    height: 180px;
    object-fit: cover;
    display: block;
}

.product__body {
    padding: 16px;
}

.product__body h2 {
    margin: 0 0 8px;
    font-size: 1.3rem;
}

.product__extras {
    padding: 12px 16px;
    border-top: 1px solid #eee;
}

.product__extras ul {
    margin: 0;
    padding-left: 18px;
}

/* 横排状态:当容器大于 480px */
@container (min-width: 480px) {
    .product {
        grid-template-columns: 280px 1fr auto;
        align-items: stretch;
    }

    .product__thumbnail img {
        height: 100%;
    }

    .product__body {
        padding-left: 20px;
        align-self: center;
    }

    .product__extras {
        border-top: none;
        border-left: 1px solid #eee;
        display: flex;
        align-items: center;
        min-width: 140px;
    }
}

/* 利用:has()给加小圆标的卡片做特殊改动 */
.product:has(.badge) .product__extras {
    display: none; /* 有徽标的卡片,不显示额外列表 */
}

.product:has(.badge) .product__thumbnail img {
    height: 220px;
}

/* 如果容器宽度大于800px,且有标记,就让额外信息重新显示为侧栏 */
@container (min-width: 800px) {
    .product:has(.badge) {
        grid-template-columns: 340px 1fr;
    }
    .product:has(.badge) .product__thumbnail {
        grid-row: 1;
    }
    .product:has(.badge) .product__extras {
        display: block;
        border-left: none;
        grid-column: 2;
        border-top: 1px solid #eee;
    }
}

这份代码里,把 container-type: inline-size 直接放在 .product 上。然后在内部用 @container 查询自身的宽度。更妙的是,:has(.badge) 可以让卡片“知道”自己左上角带了一个强调徽章,因此它隐藏了常规的额外列表。等宽度到800px时重新展示,但放在底部,不干扰主视觉。

怎么理解这个布局的联动?

我给卡片设置的初始布局是竖直排列。如果不加任何徽标,当容器宽一点(比如超过480px)时,自动变成三列(图片 280px、正文、额外列表)。这份CSS没有写任何媒体查询,它完全根据卡片自己所处的容器宽度来做布局。你把卡片放到一个窄屏里,它就是竖的;拉到宽屏主区域里,不需要你给卡片换一套class,就自动变横排。

这就是容器查询带来的直接感受。

而 :has() 处理的是卡片内部情况不同时,对布局的最终干扰。某些特殊卡有徽章,期望弱化额外功能,如果没有徽章,三者就并排。以前这种需求需要写 if else 逻辑去判断有没有徽章,再动态加类,现在一行 :has() 选择器就完事。

还存在什么样的问题?兼容性要说明

目前最想看这个技术的应该是自研后台和同时做移动端的。好在Chrome和Edge从105开始支持容器查询,Safari从16开始支持,Firefox从110版开始完整支持。现在这个时间点,公共活动页完全可以放心用。:has() 从Chrome 105、Safari 15.4、Firefox 121也开始支持,两者交叉的范围很大。

实际开发中唯一需要考虑的就是老顽固浏览器。如果有必要,给不支持容器查询的浏览器准备一个简单的竖排布局,方便降级。但不要用 @supports not (container-type: inline-size) 来重复写所有样式,极其麻烦。

拓展:容器查询 + :has() 配合动画

你还可以基于这些新特性做一些微妙的动效。比如容器在横排/竖排之间切换,可以使用transition属性对grid-template-columns做过渡?grid模板列无法用transition平滑过渡,但我们可以给内部的元素使用transform,或者用flex布局加flex-basis。简单的说,你可以在 @container 内部调整盒子的flex属性或者设置margin,自然有过渡效果。:has() 则可以帮助实现“悬停卡片内的高亮区域才把按钮颜色变深”等效果,都不用写JS。

比如:

.product:has(.product__body input:checked) {
    outline: 2px solid #f87171;
}

这种“父控件根据自己内部是否选中状态变化”的写法,对于写表单控件来说也很香。

关于文章中的代码,我为什么这么组织

上面代码故意使用了grid-template-columns,但它无法在 @container 断点之间平滑过渡,建议实际项目里用 flex 配合 flex-basis,或者把图片宽度设百分比,这样宽度切换时浏览器可以平滑动画。但这篇文章的重点是演示容器查询+has()的技术结构,不是动画设计,所以用grid展示更直观。

若想在真实项目中使用,就需要注意一点:如果你给article设置了inline-size容器后,article不能横跨整个父级宽度的100%,因为inline-size会让盒子的内联方向尺寸变成包含块?实际上不影响设置width:100%,但如果内部有百分比冲突可能产生异常。大概率没问题,但建议组件外层再套一个普通div来承担布局职责。

这种写法的前端体验提升

开发过程中,体验最明显的是:以前你为卡片写响应式样式,动不动要给一堆父级类加上最大宽度、最小宽度,还得小心翼翼,因为一旦换一个页面,卡片就可能被另一个父容器坑。现在你可以完整地把所有布局策略写在组件的样式中。它自己就是一座孤岛,在哪个区域进去都能自动调整。

尤其是设计系统里的公共组件,现在设计团队终于可以在Figma里大声说:“左边区域卡片横着放,右边区域一直竖着吧”,而开发那边只是给两个不同的父容器设置不同的宽度,结果完全是自动的。

不完美的解决方案:但确实是未来方向

容器查询和:has() 的横空出世大大缓解了组件与环境纠缠的痛点。但社区里还有一些争议,比如@container只能查询尺寸,不能查询样式状态如颜色模式,只有Style Queries才是更下一阶段的东西。不过那是后话。

不管怎样,对于现在的响应式模块,把这两样结合起来就能覆盖九成以上的场景。并且今天给出来的这个小卡片案例,基本代表了普通业务里遇到的最多的自适应烦恼。你可以自己试一试把这个文件放到一套设计系统里,直接进老页面,看看是否真的能“自己认爸”。

至少对我来说,写这类CSS比从 @media 一格格调类名,要省太多心思了。

容器查询与:has()结合:从“组件猜父级”到“父级自适应”的智能卡片实战
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css 容器查询与:has()结合:从“组件猜父级”到“父级自适应”的智能卡片实战 https://www.taomawang.com/web/css/2740.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务