你是否想过 CSS 选择器可以检测父元素下面包含特定的元素存在?例如,如果一个 card 组件包含一个缩略图,我们需要添加display:flex
样式。在之前的 CSS 中是不可能的但现在我们有了新的选择器,:has
选择器可以帮助我们选出包含特定元素的父级以及其他功能。
在这篇文章中,我会解释 :has
解决了什么问题,并通过案例来说明它是如何工作以及在哪可以使用,今天最重要的事我们如何使用它。
问题
之前根据一个元素是否存在来给父级设置样式是不可能的,我们需要根据变动来切换 CSS 的类。
考虑一下接下来的基础例子:
我们有一个 card 组件,它有两种形态:1. 有图片;2.无图片。在 CSS 中,我们需要像下面写代码:
/* A card with an image */
.card {
display: flex;
align-items: center;
gap: 1rem;
}
/* A card without an image */
.card--plain {
display: block;
border-top: 3px solid #7c93e9;
}
<!-- Card with an image -->
<div class="card">
<div class="card__image">
<img src="awameh.jpg" alt="">
</div>
<div class="card__content">
<!-- Card content here -->
</div>
</div>
<!-- Card without an image -->
<div class="card card--plain">
<div class="card__content">
<!-- Card content here -->
</div>
</div>
就像上面看到的,由于没有图片的 card 我们不需要 flex 来包裹所以创建了一个特定的 class 来指定。那现在问题是,我们是否在 CSS 中做条件判断,而不需要动态的 class ?
这就需要 :has
来救场了,它可以帮助我们 .card
是否包含 .card_image
。
例如,我们可以检测 card 元素 :has an image,若包含,我们需要给它加上 flexbox 属性。
.card:has(.card__image) {
display: flex;
align-items: center;
}
介绍 CSS :has 选择器
根据 CSS spec ,:has
选择器是检测父级是否包含指定的元素或 input 是否获取焦点。
让我们再次看看上面例子的代码片段:
.card:has(.card__image) { }
我们检测 .card
父级是否包含 .card_image
子元素。细看下面的图例:
用文字描述,上面 CSS 选择器等同于下面:
card 是否包含 card__image 元素?
是不是很神奇?我们在 CSS 中添加了逻辑,写 CSS 多么没好的时光!
:has 选择器不仅仅和 Parent 有关
它不仅仅能检测父级是否包含一个特定元素,例如,我们还可以用她检测一个元素是否跟着 <p>
元素。看看下面:
.card h2:has(+ p) { }
这就是检测了 <h2>
元素是否跟着 <p>
元素。或者我们可以用在检测 form 元素是否有一个 focused
的 input ,例如:
/* If the form has a focused input, apply the following */
form:has(input:focused) {
background-color: lightgrey;
}
浏览器支持情况
截止到写作的时间,CSS :has
只被 Safari 15.4 和 Chrome Canary 支持。保持对 Can I use 上的支持情况。
我们可否用它作为渐进增强的方案?
当然可以,我们可以通过 @support
规则来检测是否支持 :has
:
@supports selector(:has(*)) {
/* do something */
}
好了,理论只是已经讲了很多,接下来看看实际案例:
CSS :has 的案例
Section Header
当我书写 Section Header 时,经常遇到两种情况,一个只包含标题另一个既包含标题也包含链接。
依据是否存在链接,我希望添加不同的样式。
<section>
<div class="section-header">
<h2>Latest articles</h2>
<a href="/articles/>See all</a>
</div>
</section>
注意,我使用了 :has(>a)
来只选择直接子链接。
.section-header {
display: flex;
justify-content: space-between;
}
/* If there is a link, add the following */
.section-header:has(> a) {
align-items: center;
border-bottom: 1px solid;
padding-bottom: 0.5rem;
}
Card 组件例子1
让我们回顾上面的 card 例子,包含两种情况,一个包含图片而另一个不包含。
.card:has(.card__image) {
display: flex;
align-items: center;
}
我们甚至可以为不包含图片的 .card
应用特定的样式,在我们的例子中,就是 border-top
。
.card:not(:has(.card__image)) {
border-top: 3px solid #7c93e9;
}
若没有 CSS 的 :has
,我们需要两个 CSS 选择器来解决此类问题。
.card--default {
display: flex;
align-items: center;
}
.card--plain {
border-top: 3px solid #7c93e9;
}
Card 组件例子2
在这个例子中,我们有两种情况:一种是只包含链接而另一种包含多个动作(保存、分享等等)。
当 card 动作用两个不同的元素来包裹时,我们希望使用 display:flex
来布局。
<div class="card">
<div class="card__thumb><img src="cool.jpg"/></div>
<div class="card__content">
<div class="card__actions">
<div class="start">
<a href="#">Like</a>
<a href="#">Save</a>
</div>
<div class="end">
<a href="#">More</a>
</div>
</div>
</div>
</div>
.card__actions:has(.start, .end) {
display: flex;
justify-content: space-between;
}
下面是没有 CSS 的 :has
时:
.card--with-actions .card__actions {
display: flex;
justify-content: space-between;
}
Card 组件例子3
你是否遇到过 Card 组件不包含图片时,需要为内容添加上 border-radius
?这是使用 :has
的绝佳时机。
考虑下面的图例,当图片被移除时左上角、右上角的圆角变为 0,看上去有点奇怪:
/* If no image, add radius to the top left and right corners. */
.card:not(:has(img)) .card__content {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.card img {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.card__content {
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
现在看上去好多了!
没有 :has
我们必须这样做:
.card--plain .card__content {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
过滤组件
在这个例子中,我们有一个包含许多选项的组件,当没有选项被选中时,reset 按钮就不显示。然而至少一个被选中时,reset 按钮需要显示。
我们可以简单的通过 :has
来实现。
.btn-reset {
display: none;
}
.multiselect:has(input:checked) .btn-reset {
display: block;
}
根据条件显隐表单元素
我们可能需要根据前一个答案或选择来展示特定的表单字段。在这个例子中,我们一旦选择了 other
选项就需要显示 “other reason” 输入框。
通过 :has
的功能,我们可以检测是否选择了包含 other
选项来展示 “other” 字段。
.other-field {
display: block;
}
form:has(option[value="other"]:checked) .other-field {
display: block;
}
是不是很神奇?
拥有子菜单的导航
在这个案例中,我们有一个包含子菜单的导航,hover 或 focus 时展示子菜单。
我们想要的是根据是否包含子菜单来显隐箭头。我们可以通过 CSS :has
来简单的实现,思路是检测 li 是否包含 ul,若有则显示,反之隐藏。
/* Check if the <li> has a <ul>. Yes? show the arrow. */
li:has(ul) > a:after {
content: "";
/* arrow styling */
}
若没有 CSS :has
,我们可能需要单独为包含子菜单的 li 添加一个类,像下面这样:
.nav-item--with-sub > a:after {
content: "";
/* arrow styling */
}
Header 的包裹容器
当我们有一个 header 组件时,可能需要确定是占满屏幕的宽度或包含在一个容器中。
任何一种方法都需要使用 flexbox 以一种特定的方式来排列子元素。若包含 .wrapper
直接在它上应用样式,反之则需要在 .site-header
上书写样式。
<header class="site-header">
<div class="wrapper">
<!-- Header content -->
</div>
</header>
.site-header:not(:has(.wrapper)) {
display: flex;
justify-content: space-between;
align-items: center;
padding-inline: 1rem;
}
/* If it has a wrapper */
.site-header .wrapper {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1000px;
margin-inline: auto;
padding-inline: 1rem;
}
强调提醒
在一些后台看板中,有一些重要的提示需要用户注意。在那种情况下,单单的提示可能还不够我们可以为头部导航添加浅红色背景,例如:
通过 :has
我们可以检测 .main
是否包含 alert ,然后添加对应的样式。
.main:has(.alert) .header {
border-top: 2px solid red;
background-color: #fff4f4;
}
切换颜色方案
我们可以通过 :has
来切换网页颜色,例如,若我们有多种根据 CSS 变量构建的主题,可以通过切换 select 选项实现。
html {
--color-1: #9e7ec8;
--color-2: #f1dceb;
}
当我们选择列表中的其他选项时, CSS 将会如下变化:
html:has(option[value="default"]:checked) {
--color-1: #74559c;
--color-2: #f9f6fe;
}
html:has(option[value="blueish"]:checked) {
--color-1: #466ec0;
--color-2: #ebf0f7;
}
为生成的 HTML 应用样式
一些情况下,我们无法控制生成的 HTML 内容,例如:文章主题,CMS 系统可能会生成意外的内容或作者可能会嵌入视频或其它的。
假设,我们可能希望选中没有 h3 后面没有 p 标签的元素来增加一些空间。
.article-body h3:not(:has(+ p)) {
margin-bottom: 1.5rem;
}
或者我们想要选中 iframe 后面紧跟 h3 的标签,这些情况没有 :has
是不能完成的!
.article-body h3:has(+ p) {
/* do something */
}
带图标的按钮
在这个例子中,我们有一个默认的按钮样式。当有图标时,我们期望使用 flexbox 来居中和对齐按钮内容。
.button:has(.c-icon) {
display: inline-flex;
justify-content: center;
align-items: center;
}
多按钮
在一个设计系统中,我们经常会有一组按钮,若按钮数量超过 2 个,最后一个需要放到另一边。
我们可以通过 quantity queries 来实现,下面的样式会检测按钮数量是否多余 3 个,若多则会通过 margin-left:auto
把最后一个元素放到右边。
.btn-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-group:has(.button:nth-last-child(n + 3)) .button:last-child {
margin-left: auto;
}
信息模块
我在 pinterest 中看到一个这样的例子,当有错误时,我们同样需要变更标题来提醒。
.module:has(.input-error) .headline {
color: #ca3131;
}
依据条目多少来改变 Grid
通过 CSS grid ,我们使用 minmax()
来创建真正的响应式和自适应的 grid 条目。可是,那样可能还不够,我们可能还需要根据条目的多少来设置 grid 。
考虑如下图例。
.wrapper {
--item-size: 200px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--item-size), 1fr));
gap: 1rem;
}
当有 5 个条目时,最后一个会另起一行。
我们可以通过检测 .wrapper
是否包含超过 5个条目,然后使用 quantity queries 的概念。
.wrapper:has(.item:nth-last-child(n + 5)) {
--item-size: 120px;
}
.wrapper:has(.item:nth-last-child(n + 5)) {
--item-size: 120px;
}
图例和图例描述
在这个例子中,我们有一个 <figure>
,若包含 <figcaption>
,样式可能会有点不同:
- 添加一个白色背景
- 一些间隙
- 减少图片的
border-radius
figure:has(figcaption) {
padding: 0.5rem;
background-color: #fff;
box-shadow: 0 3px 10px 0 rgba(#000, 0.1);
border-radius: 3px;
}
总结:
我迫不及待的想看你们通过 :has
解决什么问题。文章中的用例只是一些基础的!我肯定后面我们会发掘一些有用的用法。
正如大家所说,这是真是一个学习 CSS 的绝佳时机。我对即将到来的新 CSS 属性很兴奋,多谢阅读!