CSS 的父选择器:has()
W3C 的 Selectors Level 4 新增了很多强大的 CSS 选择器。早在 2018 年年底就在《初探CSS 选择器Level 4》一文中和大家一起探讨了这些选择器。在这些新选择器中,最为有意思的是“逻辑组合选择器”,即 “任意匹配伪类选择器:is()
、否定(匹配无)伪类选择器:not()
、选择器权生调整伪类选择器:where()
和关系性(父选择器)伪类选择器:has()
。尤其是关系性伪类选择器:has()
,它和 CSS 容器查询在近十多年来一直成为 Web 开发者期待的 CSS 功能之一。
在这篇文章中,我将和大家一起来探讨什么是关系性伪类选择器(又称父选择器)以及它是如何工作的,并且将会通过一些示例来阐述该选择器可以在哪里,最重要的是我们现在如何使用它。
曾在《初探CSS 选择器Level 4》和《CSS 选择器
:is()
和:where()
与:has()
有什么功能》两篇博文中介绍过新增的逻辑组合选择器,感兴趣的可以先一睹为快!
简单聊一下 CSS 选择器
稍微了解 CSS 的同学都知道,要想给页面添加样式,就得使用 CSS 选择器 来选中 DOM 元素,否则添加的样式就无法运用到具体的元素上。选择器相关的知识是 CSS 领域最基础的部分,但涉及选择器的知识也很多,这一点从 W3C 有关于 选择器规范版本迭代的变更中不难发现(现在已更新到第四版本了,即 Selectors Level 4)。
社区中有关于 CSS 选择器介绍的文章也很多,我个人比较喜欢 nana (@nanacodesign)的 《CSS selectors cheatsheet & details》一文,她用图文并茂的方式介绍了 CSS 选择器常见类型:
小站上也陆续也发布了一些关于 CSS 选择器的文章,摘出一些基础和有意思的文章供大家参考:
- CSS选择器:基本选择器
- CSS选择器:属性选择器
- 再聊CSS的属性选择器
- CSS选择器:伪类选择器
- CSS伪选择器:
:empty
vs:blank
- 初探CSS 选择器Level 4
- CSS 选择器
:is()
和:where()
与:has()
有什么功能 - 编写高效 CSS 选择器
《图解CSS》系列中有关于 CSS 选择器这一章节正在编写之中,感兴趣的可以关注后续相关更新!
CSS选择器简单,而且种类繁多,但一直以来开发者都希望有一个能选中父选择器类型。那么什么是父选择器呢?感兴趣的请继续往下阅读。
父选择器:has()
是什么?
用一句话来描述: :has()
选择器是一个关系型伪类选择器,也被称为函数型伪类选择器,它和 :is()
、:not()
以及 :where()
函数型选择器被称为 CSS的逻辑组合选择器 !
注意,在规范中并没有父选择器一描述,社区中把
:has()
描述为“父选择器”是因为这样更形象,也易于理解。
我们先从其他选择器着手来介绍父选择器是什么?
正如 nana 提供的选择器示例所示,CSS 选择器中有很多类型的选择器是和 DOM 结构有关的,比如我们熟悉的 子选择器(a > b
) 、 后代选择器(a b
) 、 相邻兄弟选择器(a + b
) 、 通用兄弟选择器(a ~ b
) 、 结构伪类选择器(比如 :nth-child()
、:nth-of-type()
等) :
但在这众多的 CSS 选择器中就没有“父选择器”,或许也正因为他的缺失,很多开发者都希望有这样的一个选择器,即,能通过子元素选中其父元素。事实上,社区的开发者从未停止过这方面的探索。比如 Shaun Inman (@shauninman)早在2008年就提出了父选择器的语法规则,即 E < F
。这个语法规则看上去和CSS的子选择器有点相似,只是符号从 >
变成 <
了。
<!-- HTML -->
<a href="##">
<img src="" alt="" />
</a>
/* 子选择器 E > F */
a > img {
border: 2px solid #09f; /* 选中的是子元素 img */
}
/* 父选择器 E < F */
a < img {
border: 2px solid #09f; /* 选中的是父元素 a */
}
之后 Remy Sharp(@rem)建议使用一个 :parent
伪元素来表述父选择器的语法:
a img:parent {
border: 2px solid #09f;
}
稍微熟悉CSS选择器的开发者都知道,选择器最右位才是主体,你要样式化的东西(元素)。大多数编写CSS的人,在某种程度上,发现自己想要基于其中的内容来设计样式。按照这个规则来理解的话,@shauninman 和 @rem 提出的父选择器语法规则都超出我们的认知,比如E < F
选择器的主体是左侧的E
。
后来,Igalia公司的工程师和浏览器内核的工师提出使用 :has()
来定义父选择器的语法规则:
E:has(F) {
}
:has()
选择器看上去和 jQuery 中的:has()
选择器相似。
我们再来看一下W3C规范是怎么描述:has()
选择器:
The relational pseudo-class,
:has()
, is a functional pseudo-class taking a<forgiving-relative-selector-list>
as an argument. It represents an element if any of the relative selectors, when absolutized and evaluated with the element as the:scope
elements, would match at least one element.
大致意思是:
关系型伪类
:has()
是一个函数型伪类,接受一个选择器组(< forgive -relative-selector-list>
)作为参数。其给定的选择器参数(相对于该元素的:scope
)至少匹配一个元素。
其实,我们可以像理解jQuery中的:has()
选择器那样来理解:
:has()
选择器选取所有包含一个或多个元素在其内的元素,匹配指定的选择器。
即,:has()
选择器接受一个相对的选择器列表,如果至少有一个其他元素与列表中的选择器相匹配,那么它将代表一个元素。如果这样不好理解,可以通过下面的示例来理解。假设在我们的 HTML 中有两段这样的代码:
<!-- ① 含有卡片缩略图 img -->
<div class="card">
<img class="card__thumb" src="" alt="" />
<div class="card__content">
<h3 class="card__title">Card Title</h3>
<p class="card__describe">Card Describe</p>
</div>
</div>
<!-- ② 不含卡片缩略图 img -->
<div class="card">
<div class="card__content">
<h3 class="card__title">Card Title</h3>
<p class="card__describe">Card Describe</p>
</div>
</div>
① 和 ② 唯一的区别就是,在 ② 代码片段中不包含卡片缩略图 img
。如果我们在CSS中使用像下面这段CSS代码:
.card {
border-radius: 0.5rem;
box-shadow: 0 0.25rem 0.5rem -0.15rem hsla(0 0% 0% / 55%);
background-color: #fff;
padding: 1em 2em;
min-width: 320px;
}
.card:has(img) {
display: flex;
align-items: center;
gap: 1em;
padding: 0 2em 0 0;
}
在支持:has()
浏览器(写这篇文章为止,你可以在 Safari 15.4 或 Chrome Canary 最新版本)中看到下图这样的效果:
其中:
.card:has(img) {
display: flex;
align-items: center;
gap: 1em;
padding: 0 2em 0 0;
}
上面这段代码表示的是,含有 img
的 .card
元素重置了 .card
的 padding
值,并且添加了 Flexbox 布局相关的样式。换句话说,.card:has(img)
选择器的意思相当于 .card
元素中包含了img
元素吗? 简单地说,在CSS的选择器中有了一个条件判断的逻辑:
if (.card元素包含了img元素) {
.card {
display: flex;
align-items: center;
gap: 1em;
padding: 0 2em 0 0;
}
} else {
.card {
padding: 1em 2em;
}
}
很神奇吧!
父选择器为何会缺失这么久?
父选择器和容器查询特性在近十多年来一直都是众多Web开发者所期待的特性,如果你一直有关注 CSS 状态发展相关的报告,不难发现父选择器和容器查询特性都一直排列前列:
既然“父选择器”众人期待,而且又是那么实用,怎么在 CSS 选择器中一直就没有“父选择器”呢?
正如 Jonathan Snook(@snookca)在2010年的一篇文章中描述的那样,这不仅是因为性能方面的考虑,还因为浏览器引擎渲染文档并将计算样式应用于元素的方式:作为一个流,一个元素接一个元素进入。
上面视频来自于 Ponime 在 YouTube 上发布的视频:Gecko Reflow Visualization - mozilla.org
正如上面视频所演示的那样,当一个元素在浏览器屏幕上渲染出来时,它的父元素以及父元素渲染好的UI样式都已经在那里了。在此之后,重新绘制父节点(以及所有其他父节点)将需要对所有父节点选择器进行另一计算。这样的计算对渲染引擎来说是昂贵的!
曾在《初探 CSS 渲染引擎》和《理解 Web 的重排和重绘》两篇博文中有涉猎过这方面的知识。
之前在整理有关于CSS选择器对性能影响文章(《编写高效 CSS 选择器》)时,发现CSS的通用选择器(*
)是效率最低的CSS选择器。也正如乔纳森.斯努克(Jonathan Snook)说,
如果有一个父选择器,那将很容易成为低效率选择器中的新老大。
其理由是,当从页面中动态地添加和删除元素时,可能会导致整个文档需要重新渲染(主要是内存使用方面的问题),即很容易产生重绘和重排。即使如此,乔纳森.斯努克(Jonathan Snook)仍然很乐观:
我所描述的在技术上并非不可能。事实上,恰恰相反。这只是意味着我们必须处理使用这种特性所带来的性能影响。
这观点后来也得到了 Eric Meyer(@meyerweb)印证,“性能问题可能已经解决了”!Eric Meyer 在 Twitter上发了一条信息,提到了如何避免一直困扰着父选择器给渲染带来的性能问题。
如果对于该话题感兴趣的话,可以直接观看 Byungwoo Lee(@byungwoo_bw)在YouTube发的视频《'has' prototyping status》,视频中对应的 PPT 可以点击这里查阅。Byungwoo Lee 还专门用了两篇文章(《CSS Selectors :has()
:A way of selecting the parent element》和《How blink tests :has()
pseudo class:How to use cache to control :has()
complexity》)介绍 :has()
的使用、存在问题以及如何使用缓存来控制:has()
的复杂性等。
简而言之,浏览器渲染引擎的策略就好像是下象棋一要,应该快速找到如何忽略无关的走法,而不是预测每种走法组合的所有可能结果。对于 CSS 而言,渲染引擎会防止对不相关元素的计算。为了减少应用样式后不相关的重新计算,渲染引擎可以在重新计算期间标记样式是否受到:has()
状态更改的影响。
另外,这几年浏览器的渲染引擎已经有了相当大的改进。渲染过程已经被优化到浏览器可以有效地确定哪些需要渲染或更新,哪些不需要,从而为一系列新的和令人兴奋的功能开辟了道路。正因为这些的改进,有机会让 :has()
看到曙光。到目前为止,你可以在 Safari 15.4+ 和 Chrome Canary(写这篇文章时是 103.0.5011.0 版本)可以看到 :has()
效果。同时也希望Firefox和Chrome也能快速跟上。
说个题外话,最早实现 :has()
选择器的浏览是 Safari,正如 Jen Simmons(@jensimmons)在 Twitter 所言:“不要老说Safari总是最后一个。有时我们是第一”。
自从Jen Simmons加入Safari 的Web开发者体验团队之后,Webkit内核在CSS特性上的更新速度较之前有显著的变化。很多新特性都是先在 Safari 上看到的。
父选择器的使用
在 2021年 05 月 Brian Kardell(@briankardell)就宣布,Igalia的团队目前正在制作一个:has()
选择器的原型:
Igalia 的团队在浏览器渲染引擎上实现了很多强大的特性,比如 CSS的 Grid、Subgrid、容器查询等。
Brian Kardell 在他的文章《Can I :has()
》中提到过:“:has()
选择器将作为一个父选择器,但它可能有更广泛的用途,超出了这个范围”。:has()
选择器除了像前面的卡片示例所演示的功能之外(根据DOM树中的子元素)还可以根据DOM树中的后续元素的内容或状态来选择元素。也就是说,:has()
选择器对于根据 DOM 树中的子元素或后续元素的内容或状态有条件地将样式应用于UI组件是非常有用的。期待已久的:has()
选择器可以扩展现有选择器的范围和使用场景,提高CSS的质量和健壮性,并减少对JavaScript的依赖和CSS类的需要。
既然如此,我们先来深入了解一下:has()
的使用。我将会通一些简单地示例来向大家展示 :has()
选择器如何使用?
在开始之前,我们回忆一下前面的示例使用了 .card:has(img)
,其中放置在:has()
中()
的选择器img
和在:has()
的 :
前的选择器.card
是一种包含关系,两者之间的关系又和DOM树结构是有着紧密关联的。如果我们把.card:has(img)
选择器换成.card:has(>img)
呢?它们分别对应的关系是:
.card:has(img)
表示的是.card
元素包含了img
元素,该img
元素只要是.card
的后代元素(包括子元素),则条件就是成立的,也就会选中.card
元素.card:has(> img)
表示的是.card
元素包含了img
元素,且img
元素是.card
的子元素,只有这样条件才能成立,才能选中.card
元素
把上面的示例稍作调整:
<!-- ① 含有卡片缩略图 img -->
<div class="card">
<img class="card__thumb" src="" alt="" />
<div class="card__content">
<h3 class="card__title">Card Title</h3>
<p class="card__describe">Card Describe</p>
</div>
</div>
<!-- ② 不含卡片缩略图 img -->
<div class="card">
<div class="card__thumb">
<img src="" alt="" />
</div>
<div class="card__content">
<h3 class="card__title">Card Title</h3>
<p class="card__describe">Card Describe</p>
</div>
</div>
① 和 ② 模板对应的DOM树如下所示:
给.card:has(img)
和.card:has(>img)
下改变.card
的background-color
:
.card {
background-color: #fff;
}
.card:has(img) {
background-color: #09f;
color: #fff;
text-shadow: 1px 1px rgb(0 0 0 / 50%);
}
.card:has(> img) {
background-color: #f36;
color: #fff;
text-shadow: 1px 1px rgb(0 0 0 / 50%);
}
在支持和不支持:has()
的浏览器你将看到不一样的效果:
从上图效果来看,.card:has(img)
和.card:has(> img)
都符合条件,并且将样式运用到 .card
元素上。当然,我们也要注意其选择器权重的问题,如果我们把 .card:has(img)
和 .card:has(> img)
顺序更换一下:
.card:has(> img) {
background-color: #f36;
color: #fff;
text-shadow: 1px 1px rgb(0 0 0 / 50%);
}
.card:has(img) {
background-color: #09f;
color: #fff;
text-shadow: 1px 1px rgb(0 0 0 / 50%);
}
你可能已经想到了,两个.card
的background-color
都会是 #09f
。这主要是因为,.card:has(img)
和 .card:has(> img)
都符合条件,都会选中.card
元素,这个时候谁被运用,就要看选择器权重(0, 1, 1
)以及在源码中顺序了。.card:has(img)
出现在.card:has(> img)
后面,将会覆盖前面.card:has(>img)
样式:
在传给:has()
选择器参数时,可以是任何选择器。比如:
/* 选择有 img 作为后代元素的 .card 元素 */
.card:has(img) {}
/* 选择有 img 作为子元素的 .card 元素 */
.card:has(> img) {}
/* 选择有类名为 .card__thumb 作为后代元素的 .card 元素 */
.card:has(.card__thumb) {}
/* 选择有一个 h2 元素后紧跟一个 p 元素的 .card 元素 */
.card:has(h2 + p) {}
/* 选择有一个 h2 元素,且后面有 p 元素的 .card 元素 */
.card:has(h2 ~ p){}
从这些简单的示例代码中不难发现,:has()
选择器和否定伪类选择器:not()
有点类似,关系型伪类选择器 :has()
有两部分组成:
<target_element>:has(<selector>) {
/* ... */
}
这两个部分的意思大致是:
- ①:
<target_element>
字面意思是“目标元素”,也就是样式规则要样式化的主体。即,它是一个元素的选择器,如果作为参数传递给:has
伪类条件得到满足(true
),该元素将成为目标。条件选择器的范围是这个元素(<selector>
不会离开<target_element>
所在的范围) - ②:
<selector>
指的是一个用CSS选择器定义的条件,需要满足这个条件才能将样式应用到目标元素<target_element>
上
上面我们看到的示例都是展示如何使用:has()
选择器来选中目标元素,其实它也可以和其它CSS选择器组合起来使用,用来选中目标元素的子元素或相邻元素。比如:
/* 如果 figure 元素有一个 figcaption 后代元素,则选中 figure 元素中的 img 元素 */
figure:has(figcaption) img {
/* 样式被运用于 模板② 中的 img */
}
<!-- ① 只有 img -->
<figure>
<img src="w3cplus.png" alt="The beautiful W3cplus logo." /> <!-- ✗ -->
</figure>
<!-- ② 带有标题的 img -->
<figure>
<img src="w3cplus.png" alt="The beautiful W3cplus logo." /> <!-- ✔ -->
<figcaption>W3cplus Logo</figcaption>
</figure>
对于像下图这样的场景,:has()
选择器就非常实用了:
再来看一个状态切换的样式变化的示例:
form button {
/* 按钮默认样式,比如未选中状态的样式 */
}
form:has(input[type="checkbox"]:checked) button {
/* 复选框选中状态的按钮样式 */
}
上面示例代码中使用:has()
改变按钮 button
样式,比如一个注册表单,只有用户同意相关注册协议(表单中的筛选框被选中),创建账号的按钮才高亮,变成可用状态,否则就是处于禁用状态:
这个示例告诉大家,:has()
可以根据子元素(后代元素)的状态(比如我们熟悉的 :hover
、:active
、:visited
、:focus
等)来设计不一样的样式。
你甚至还可以与一些表单验证相关的CSS伪类结合在一起,重新定义表单的样式:
从这几个例子中,可以看出关系型伪类选择器:has()
是多么通用、强大和实用。它甚至可以与其他伪类结合起来(比如:not()
)创建更复杂的关系选择器。比如:
/* 选择不包含空元素的卡片元素 */
.card:not(:has(*:empty)) {}
/* 选择至少有一个复选框未选中的表单元素 */
form:has(input[type="checkbox"]:not(:checked)){}
/* 选择不包含标题元素的卡片元素 */
.card:not(:has(h1, h2, h3, h4, h5, h6)){}
当 :has()
和 :not()
组合在一起使用的时候,两者的排序非常的重要,即两者的嵌套直接会影响最终匹配的结果。比如下面这两个组合:
/* 匹配不包含任何标题元素的 section 元素 */
section:not(:has(h1, h2, h3, h4, h5, h6)){}
/* 匹配任何包含非标题元素的 section 元素*/
section:has(:not(h1, h2, h3, h4, h5, h6)){}
使用Flexbox布局时,当卡片数量不是列数的倍数时,最后一排卡片宽度会像下面这个视频效果一样变化:
虽然在《如何编写防御式的 CSS》一文中介绍了如何避免这种现象。除此之外,还可以使用:has()
伪类选择器与结构型伪类选择器组合在一起,比如:
section > div {
flex: 1 1 calc((100% - 10px * 2) / 3);
}
section:has(div:nth-child(3n + 1):last-child)::after {
flex: 1 1 calc(((100% - 10px * 2) / 3) * 2 + 10px);
}
section:has(div:nth-child(3n + 2):last-child)::after {
flex: 1 1 calc((100% - 10px * 2) / 3);
}
根据在<section>
中的 <div>
元素数量,给section
的::after
元素设置不同的 flex-basis
的值。
注意,示例中的 :nth-child(3n + 1):last-child
和 :nth-child(3n + 2):last-child
是结构伪类选择器的一种特性,这种特性被称为 数量查询(也称为 范围选择器)。这种特性也适用于 :nth-of-type()
和:nth-last-of-type
选择器。如果你对该特性感兴趣的话,可以阅读:
- CSS选择器:伪类选择器
- Quantity Queries for CSS
- Using CSS Mod Queries with Range Selectors
- Constructing CSS Quantity Queries On The Fly
- Use Quantity Queries to Make Your CSS Quantity-Aware
- Tidy list using Mod Queries and Range Selectors
上面演示的示例代码大部分都是用来展示如何通过:has()
选择器的<selector>
参数匹配(选中)目标选择器(目标元素)<target_element>
,其实关系型选择器:has()
绝不仅限于目标元素的子元素的内容和状态,还可以针对 DOM 树中的相邻元素,有效的地使用它为一个“上一个兄弟选择器”。
注意,我们熟悉的相邻兄弟选择器
+
和通用兄弟选择器~
一般都是用来选择该元素之后的兄弟元素。
比如下面这几个示例:
/* p元素后面紧跟有 img 元素 */
p:has(+img) { }
/* img 元素后面跟随了没有运用 hidden 类的 figcaption 元素 */
img:has(~figcaption:not(.hidden)) { }
/* label 元素后面跟限了没有焦点状态的 input 元素 */
label:has(~input:not(:focus)) { }
我们还可以多个:has()
以链式方式使用,比如:
/* 匹配同时包含了 h2 和 p 元素的 section 元素 */
section:has(h2):has(p){}
我们可以把上面选择器简写成section:has(h2, p)
,用逗号来分隔,即将一个组合选择器传递给了:has()
选择器。
另外有一个有意思的地方是,如果你传递给:has()
一个无效的选择器时,这个无效的选择器会被忽略,比如:
article:has(h2, p, ::-blahdeath) {
/* ::-blahdeath 是一个无效的选择器,它会被忽略,最终相当于是 article:has(h2, p) */
}
上面这些示例都是用来告诉大家如何使用:has()
这个关系型伪类选择器,从上面的示例中不难发现,它选中的并不仅是父元素,也可以根据匹配条件选择上一个兄弟元素,所以很多CSSer专家以及:has()
规范设计者都建议不把 :has()
选择器称为父选择器,更为精确或符合规范的称呼是 “关系型伪类选择器”。
我们可以给 :has()
传递任何选择器,也可能是一个组合,但不管传递是简单还是复杂的选择器,它和目标选择器在 DOM 上都有一定的关系,可能是父子、后代或兄弟等关系。而且,它最终将CSS选择器锚定在一个带有:has()
伪类的元素上,并防止选择移移动到作为伪类参数传递的元素上。简单地说,它永远被选中的是:has()
前面的元素,即 <target_element>
元素,传递给他的<selector>
只是一个匹配条件。
.card img {
/* 选中的是 .card 元素下的 img 元素;img 是目标元素 */
}
.card:has(img) {
/* 选中的是包含了 img 元素的 .card 元素;.card 元素是目标元素 */
}
img + figcaption {
/* 选中紧跟在 img 元素后面的 figcaption 元素; figcaption 元素是目标元素 */
}
img:has(+figcaption){
/* 选中后面紧跟了 figcaption 元素的 img 元素; img 元素是目标元素 */
}
到止前为止,我们只能在 Safari 15.4+ 和 Chrome Canary 最新版本 以及即将到来的 Chrome 101版看到 :has()
选择器的效果。要得到所有主流浏览器的支持还是有一段时间。在此期间,我们可以再次使用渐进增强技术,在支持他的浏览器中使用 :has()
。在CSS中我们可以使用条件CSS @supports
的 selector()
函数来查询:has()
是否得到浏览器的支持:
@supports selector(:has(*)) {
.alert {
display: none;
}
}
注意,
selector()
函数是@supports
的一个新增特性,我在编写《图解CSS:条件 CSS》章节时,@supports
还没有该特性。它是在 CSS Conditional Rules Module Level 4 中增加的。如果你对这个特性感兴趣,可以阅读 Chris Coyier 的《@supports selector()》一文。
Adam Argyle(@argyleink)在他的推文中提到过,可以像下面这样检测:has()
是否得到浏览器支持:
@supports selector(:has(works)) {
/* CSS Code... */
}
传给:has()
选择器的是一个works
字符串,也可以是其他字符串,它是不是一个元素的名称,或选择器名称,浏览引擎并不关心!
父选择器能解决什么问题?
CSS 技能对于一位 UI 开发者是尤其的重要,如何使用 CSS 开发更具健壮性和扩展性的 UI,也是 UI 开发者必备技能之一。
如果你对如何让自己开发的 UI 更健壮,更具扩展性的话题感兴趣的话,可以移步阅读今年年初发表的一篇关于这方面话题的文章:《如何编写防御式的 CSS》!
先不说,如何开发更具扩展性的 UI 和如何编写防御式的 CSS吧。接下来我想和大家聊的是,关系型选择器 :has()
能帮我们解决些什么问题?我想我还是以一些常见的 UI 示例来回答这个问题吧。
先来看案例一。我们平时在还原 UI 的时候,像下图这样的 UI 场景应该很常见:
就如上图中所展示的三组卡片,每组卡片之间输出的数据不同(DOM不同),每组卡片会因为数据字段不同,UI 风格也会略有不同,甚至会有较大的 UI 风格差异。以往我们要实现这样的 UI 效果,需要在不同的元素上添加不一样的类名。就拿第一组来说吧,两张卡片相比,上面的卡片多了描述文本(它可能是一个<p>
元素)和一组媒体信息(它可有是一个<ul>
),但最终呈现给用户的 UI 风格来说,一张是竖排,另一张是横排。按以往开发模式,可能会在两个不同的卡片上添加不一样的类名:
也就是说,如果希望根据一个元素的存在与否来给一个特定的父级或元素设置不同的样式是不可能的。我们需要像上图那样添加额外的类名,并根据UI的需要来切换它们。就拿上图来说吧,它的 DOM 结构可能像下面这样:
<!-- ①: 带有描述信息和媒体信息的卡片 -->
<div class="card card—vertical">
<div class="card__media">
<div class="media__object">
<img src="https://picsum.photos/400/400?random=2" alt="" class="media__thumb">
</div>
<div class="media__content">
<h3 class="media__title">Kenneth Erickson</h3>
</div>
<div class="media__action">
<svg class="icon--more"></svg>
</div>
</div>
<div class="card__body">
<p class="card__description">The word "coffee" entered the English language in 1582 via the Ddutch koffie</p>
</div>
<div class="card__footer">
<ul class="card__social">
<li>
<svg class="icon--like"></svg> 783 Likes
</li>
<li>
<svg class="icon--comment"></svg> 67 Comments
</li>
</ul>
</div>
</div>
<!-- ②: 带有子标题,没有描述和媒体信息 -->
<div class="card card—horizontal">
<div class="card__media">
<div class="media__object">
<img src="https://picsum.photos/400/400?random=2" alt="" class="media__thumb">
</div>
<div class="media__content">
<h3 class="media__title">Kenneth Erickson</h3>
<h5 class="media__subtitle">San Diego,CA</h5>
</div>
<div class="media__action">
<svg class="icon--more"></svg>
</div>
</div>
</div>
或许你会通过不同的类名来改变 Flexbox 的布局,比如:
/* 默认水平排列,且垂直居中 */
.card {
display: flex;
align-items: center;
}
/* 在卡片 ① 上使用下面代码,将水平排列换成垂直排列 */
.card—vertical {
flex-direction: column;
align-items: flex-start;
}
问题是,如果 CSS 自身就具备条件判断,那就不需要像上面那样额外的添加类名。那么,关系型伪类 :has()
在这样的场景之下就有用武之地了。
正如前面所介绍,我们可以使用 :has()
来做一定的条件判断,比如说,如果 .card
元素中包含了 p
元素或包含一个ul
元素,我们就改变 Flexbox 的布局方式:
.card {
display: flex;
align-items: center;
}
.card:has(p, ul) {
flex-direction: column;
align-items: flex-start;
}
当然,你也可以使用相关的类选择器:
.card:has(.card__description, .card__social) {
flex-direction: column;
align-items: flex-start;
}
这个示例中,卡片 ② 中是没有任何元素命名类名为 .card__description
或 .card__social
。
示例使用 :has()
的选择器的代码:
.card:has(p, ul) {
flex-direction: column;
align-items: flex-start;
}
.card:has(p, ul) .media__object {
width: 32px;
}
.card__media:not(:has(.media__subtitle)) {
font-size: 12px;
}
.card__media:not(:has(.media__subtitle)) .icon--more {
font-size: 24px;
}
@supports not selector(:has(works)) {
.card {
flex-direction: column;
align-items: flex-start;
}
}
具有差异化 UI 效果如下:
再来看一个有关于表单方面的示例。比如:
为了能给用户一个更好的体验,用户在填写表单内容时,通过一个指示器或其他UI表达信息,来告诉用户完成度和正确度。比如上图的效果,只有用户填写完成,并且符合要求,表单按钮(“SIGN IN”)才高亮可点。以往仅用 CSS 是无法实现这样的UI效果(交互效果),需要借助 JavaScript 来处理。不过有了关系型选择器:has()
之后,我们可以根据表单验证相在的伪类选择器,比如 :valid
、 :invalid
和 :checked
等实现上图的效果。
我想通过这两个简单示例告诉大家的是:
关系型伪类选择器
:has()
可以不再需要因为内容、状态、有效性等添加额外的类名或借助JavaScript脚本来实现具有差异性的 UI 表达。
换句话说,关系型伪类选择器:has()
可以有条件的让UI具有差异化表达能力。可能根据动态的内容,状态的切换等调整UI效果,让你的 UI 更具扩展性和灵活性。
父选择器案例
最后我们来看一看能使用关系型伪类选择器 :has()
的具体案例。
Ahmad ShadeedAhmad (@shadeed9)在他新发的文章《CSS Parent Selector》提供了多个使用
:has()
的案例!
我们可以在 @shadeed9 的基础上可以细分一下 :has()
的使用场景,大致可以分为:
- 基于内容的变化
- 基于状态的变化
- 基于验证的变化
基于内容的变化
前面向大家演示:has()
的示例,大部分就是基于内容的变化。开发 Web UI的时候,一些 UI 会因为内容不同而有很多种变化。比如下面这些场景,我想大家并不陌生:
上图中的六组都是来自于 Web 中常见的 UI效果。它们有着共同的一个特色:UI效果或布局因内容的不同效果不同。前面也说过了,以往实现这样的效果,通常会在不同的容器上创建多个类名来覆盖有可能的变化,并根据不同的方法和技术栈,手动或依赖JavaScript脚本来应用它们。
有了关系型选择器:has()
,开发者就可以直接在CSS中对内容进行检测,样式就会自动应用,比如上图中的第一组,购物列表页会因为有无清单,展示不同的效果。这将减少变化的 CSS 类的数量,减少人为错误造成的错误的可能性,而且选择器将通过条件检测进行自我记录。
购物列表页
同一个页面,因为内容不同展示两种不同的效果。在没有购物列表时(比如,.shopping__lists
)时,页面主内容水平垂直居中:
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
在含有 .shopping__lists
的 main
覆盖Flexbox的布局,
main:has(.shopping__lists) {
align-items: flex-start;
flex-direction: row;
min-height: 0;
}
看到的效果如下:
示例中使用了 CSS 的滚动捕捉特性,让滚动体验更佳,如果你对这方面的特性感兴趣的话,可以移步阅读《图解CSS:CSS滚动捕捉(Part1)》和《图解CSS:CSS滚动捕捉(Part2)》。
标题栏或导航栏
上图所展示的UI风格,我想大家很熟悉吧!我们拿H5页面的导航栏为例。我们来看使用 :has()
来实现四种差异化的导航栏效果:
header {
display: flex;
align-items: center;
justify-content: center;
color: #d1c3af;
background-color: #141414;
padding: 0 24px;
gap: 14px;
}
header:has(button + h1) {
justify-content: flex-start;
background-image: linear-gradient(90deg, #d39f96 0%, #dfaa9b 100%);
color: #454545;
}
header:has(h1 + button) {
justify-content: space-between;
background-color: #e20200;
color: #fff;
}
header:has(h1 + .header__buttons) {
justify-content: space-between;
background-image: linear-gradient(90deg, #fe4529 0%, #ff4e0a 100%);
color: #fff;
position: relative;
}
从上往下,在 :has()
选择器传入不同的参数(选择器)来改变其对齐方式、背景和文本颜色:
header:has(button + h1)
将匹配button
后紧跟有h1
元素的header
,刚好符合第二个UIheader:has(h1 + button)
将匹配h1
后紧跟有button
元素的header
,刚好符合第三个UIheader:has(h1 + .header__buttons)
将匹配h1
后紧跟有.header__buttons
元素的header
,刚好符合第四个 UI
在支持 :has()
选择器的浏览器,你将看到的效果如下:
从示例代码中你可能已经发现了,第三个和第四个导航栏,采用的都是两端对齐,但这对于第四个 UI 来说,仅采用两端对齐并不能满足标题水平居中的效果,为此对标题采用了绝对定位,同时为了防止标题和右侧图标重叠,可以给标题添加一个限制,比如说最多展示五个中文字:
header:has(h1 + .header__buttons) h1 {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
max-width: 5em;
overflow: hidden;
text-overflow: ellipsis;
}
至于为什么第四个标题采用绝对定位,这主要是因为右侧是多个图标,他和左右一个图标的宽度不一样,因为 Flex 项目宽度计算,会造成标题更偏左,具体的可以参阅:
- 图解CSS:Flexbox布局(Part1)
- 图解CSS:Flexbox布局(Part2)
- Flexbox布局中不为人知的细节
- 聊聊Flexbox布局中的
flex
的演算法 - 你真的了解CSS的
flex-basis
吗?
卡片组件
上图是一个卡片组件,同样因为内容字段不同,调整整相应UI,比如图片尺寸,文本尺寸和网格布局分布:
我们把卡片 ① 当作是默认的样式:
article {
display: grid;
grid-template-columns: 50% auto;
grid-template-areas: "figure content";
align-items: stretch;
}
它和其他五张卡片在信息表达上有一定的差异,它没有描述内容,比如 <p>
元素(根据你自己的 HTML 结构来定),而且它的内容区域垂直居中,可以将 :not()
和 :has()
结合起来使用:
article:not(:has(p)) .article__content {
align-self: center;
}
对于包含 p
元素的 article
重新使用 grid-template-columns
定义网格列轨道的值:
article:has(p) {
grid-template-columns: 30% auto;
}
而卡片 ③ 和其他卡片也有一个较为显著的信息差异,图片上多有一个描述信息,比如多一个 figcaption
元素,这样我们就可以针对饮食 figcaption
元素卡片定义样式的差异:
article:has(figcaption) figure {
position: relative;
}
article:has(figcaption) figcaption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
color: #f5f5f5;
background: rgba(0, 0, 0, 0.6);
font-size: 0.75em;
padding: 1em 2em;
border-radius: 0 0 0 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
卡片 ④ 和 卡片 ⑤,UI上的特色较为明显:
- 卡片 ④ 是一个两栏的布局
- 卡片 ⑤ 没有缩略图
针对 卡片 ④ 和 卡片 ⑤ 我们可以像下面这样的写样式:
article:has(aside, main) {
background: transparent;
border-radius: 0;
box-shadow: none;
grid-template-columns: repeat(2, 1fr);
gap: 1.5em;
}
article:has(aside, main) > * {
background: #f5f5f5;
border-radius: 12px;
box-shadow: 0 0 0.25em rgb(0 0 0 / 25%);
}
article:has(aside, main) figure,
article:has(aside, main) img {
border-radius: 12px 12px 0 0;
}
article:has(aside, main) main {
padding: 1.5em 2em 2em;
}
article:not(:has(figure)) {
grid-template-columns: auto;
grid-template-areas: none;
}
最终的效果如下:
在支持的浏览器中,你看到的效果如下:
上面这个示例,如果不使用
:has()
来编写样式,可能需要添加很多额外的类名,详细的可以参阅 @Smashing Magazine 在 Codepen 上写的 Card variations 示例!
图片分享
在一些社交媒体(比如微信朋友圈,微博等)分享图片时,希望发布 1 ~ 9
张图片时,其布局方式不一样。让图片有一个更好的展现方式,比如:
从设计效果中我们可以发现,我们把图片容器用12
列网格的形式来描述,并且:
- 一张图片时,图片宽度等于容器宽度(跨
12
列网格) - 两张图片时,每张图片宽度跨
6
列网格 - 三张图片时,每张图片宽度跨
4
列网格 - 四张图片时,第四张图片宽度跨
12
列网格 - 五张图片时,最后两张图片宽度跨
6
列网格 - 六张图片时,表现形式和三张图片相同,每张图片宽度跨
4
列网格 - 七张图片时,表现形式和四张图片相同,最后一张图片宽度跨
12
列网格 - 八张图片时,表现形式和五张图片相同,最后两张图片宽度跨
6
列网格 - 九张图片时,表现表式和三张图片、六张图片相同,每张图片宽度跨
4
列网格
时至今日,可以直接使用 CSS Grid 来构建12
列网格布局:
.figures {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 10px;
grid-auto-flow: row;
}
有关于 CSS Grid 网格布局相关的知识这里就不花时间阐述了,如果你感兴趣的话,可以从《2022年不能再错过 CSS 网格布局了》一文中获取。
默认你将能看到的效果如下图所示:
上图效果并不是我们预期想要的,以还没 :has()
选择器之前,需要使用添加额外的类名,比如 .figures--1
到 .figures--9
之类的类名或者是其他你自己喜欢的类名。不过,这里用该效果是用来阐述 :has()
的使用和作用。因此,该示例并不会添加额外的类名来实现,我们将会采用 :has()
选择器和 CSS结构伪类选择器:nth-child
和:last-child
组合完成。
为什么要使用结构性伪类选择器呢?在前面有所提到了,我们可以使用像下面这样的选择器来对DOM的元素数量进行查询,比如:
li:nth-child(1):last-child
,它的意思是,li
既是ul
第一个子元素,又是ul
的最后一个子元素,即表示ul
只有一个li
子元素li:nth-child(2):last-child
,它的意思和li:nth-child(1):last-child
相似,表示的是li
是ul
的第二个子元素,也是它的最后一个子元素,即表示ul
包含了两个li
按照类似方式,我们就可以查询出 ul
中有多少个 li
。假设我们的 Demo结构像下面这样:
<!-- HTML -->
<section class="figures">
<figure>
<img src="https://picsum.photos/1280/1024?random=1" alt="">
</figure>
</section>
<section class="figures">
<figure>
<img src="https://picsum.photos/1280/1024?random=2" alt="">
</figure>
<figure>
<img src="https://picsum.photos/1280/1024?random=3" alt="">
</figure>
</section>
那么我们就可以使用 .figures figure:nth-child(1):last-child
来查询出 .figures
中有几个 figure
元素,也就可以查询出 .figures
容器中呈现的图片数量。
更为有意思的是,我们这个示例较为特殊,从 1
张到 9
张,我们可以用更为简单的结构伪类选择器来表示,比如 .figures figure:nth-child(3n):last-child
、 .figures figure:nth-child(3n + 1):last-child
和 .figures figure:nth-child(3n + 2):last-child
。示例中的图片数量始终是等于3n
、 3n + 1
或 3n + 2
:
- 容器中分别有三张、六张和九张图时,相当于
3n
- 容器中分别有一张、四张和七张图时,相当于
3n + 1
- 容器中分别有两张和八张图时,相当于
3n + 2
注意,在 CSS 结构伪类选择器 :nth-child()
(或 :nth-of-type()
) 中的 n
是从 0
开始索引。
在此基础上,结合:has()
关系选择器,事情就简单多了:
.figures:has(figure:nth-child(3n):last-child)
可以匹配到包含3
、6
和9
个figure
元素的.figures
.figures:has(figure:nth-child(3n + 1):last-child)
可以匹配到包含1
、4
和7
个figure
元素的.figures
.figures:has(figure:nth-child(3n + 2):last-child)
可以匹配到包含2
、5
和8
个figure
元素的.figures
这样一来,1 ~ 9
都能匹配上了。选中了相应元素之后,只需要调整图片跨列的网格列数,在 CSS Grid 布局中,可以使用 span
关键词来实现:
/* 创建12列网格,每列宽度是一个 fr 单位,间距是 10px */
.figures {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 10px;
grid-auto-flow: row;
}
/* 默认图片宽度跨 4 列 */
.figures figure {
grid-column-end: span 4;
}
/* 图片宽高比默认是 1:1 */
.figures img {
aspect-ratio: 1;
}
/* 图片容器中分别包含 3、 6 和 9 张图片时,图片宽度跨四列,和默认状态相同 */
.figures:has(figure:nth-child(3n):last-child) figure {
grid-column-end: span 4;
}
/* 图片容器中分别包含 3、 6 和 9 张图片时,图片宽高比是 1:1,和默认状态相同 */
.figures:has(figure:nth-child(3n):last-child) img {
aspect-ratio: 1;
}
/* 图片容器中分别包含 1、 4 和 7 张图片时,图片宽度跨四列,和默认状态相同 */
.figures:has(figure:nth-child(3n + 1):last-child) figure {
grid-column-end: span 4;
}
/* 图片容器中分别包含 1、 4 和 7 张图片时,图片宽度跨四列,和默认状态相同 */
.figures:has(figure:nth-child(3n + 1):last-child) img {
aspect-ratio: 1;
}
/* 图片容器中分别包含 1、 4 和 7 张图片时,最后一张图片宽度跨 12 列 */
.figures:has(figure:nth-child(3n + 1):last-child) figure:last-child {
grid-column-end: span 12;
}
/* 图片容器中分别包含 1、 4 和 7 张图片时,最后一张图片宽高比 16:9 */
.figures:has(figure:nth-child(3n + 1):last-child) figure:last-child img {
aspect-ratio: 16 / 9;
}
/* 图片容器中分别包含 2、 5 和 8 张图片时,图片宽度跨四列,和默认状态相同 */
.figures:has(figure:nth-child(3n + 2):last-child) figure {
grid-column-end: span 4;
}
/* 图片容器中分别包含 2、 5 和 8 张图片时,图片宽高比是 1:1 */
.figures:has(figure:nth-child(3n + 2):last-child) img {
aspect-ratio: 1;
}
/* 图片容器中分别包含 2、 5 和 8 张图片时,最后两张图片宽度跨6列 */
.figures:has(figure:nth-child(3n + 2):last-child) figure:nth-last-child(1),
.figures:has(figure:nth-child(3n + 2):last-child) figure:nth-last-child(2) {
grid-column-end: span 6;
}
/* 图片容器中分别包含 2、 5 和 8 张图片时,最后两张图片宽高比是 4:3 */
.figures:has(figure:nth-child(3n + 2):last-child) figure:nth-last-child(1) img,
.figures:has(figure:nth-child(3n + 2):last-child) figure:nth-last-child(2) img {
aspect-ratio: 4 / 3;
}
在支持:has()
浏览器上看到的效果和我们设计图提供的效果是一致的:
基于状态的变化
在 CSS 选择器中有一种状态伪类选择器,比如早期:
- 用于
<a>
元素的:hover
、:focus
、:active
和:visited
等 - 用于表单控件的
:focus
、:checked
和:disabled
等
除了上述这些状态伪类选择器之外, CSS 还新增了用于焦点管理的状态伪类选择器,比如 :focus-within
、:focus-visible
和锚点伪类选择器:target
。其中:focus-within
可以利用焦点元素在获得焦点状态时改变其父元素(或祖先元素)的样式,看上去有点像关系型伪类选择器:has()
。
曾在《CSS
:focus-within
》一文中详细介绍过:focus-within
怎么选中父元素。
如果我们把关系型伪类选择器:has()
和这些状态伪类选择器结合起来使用,会让你在UI的交互和展示上得到一些意想不到的效果。比如下面这些场景。
过滤组件
上图是一个过滤组件的效果图,左侧是没有过滤项选中的UI,右侧是有一个或多个过滤项选中的UI。有选项选中时,顶部区域右侧有相应的变化,比如重置按钮显示出来、显式具体选中的数量 和 图标替换等。以往实现这些效果,一般都是依赖于 JavaScript 来实现的,不过,接下来我们来看 :has()
是如何让纯 CSS 实现这样的组件交互效果。
这里向大家展示上图中第二个效果,这是基于 Ahmad Shadeed 的提供的示例改进得来的。
当 input[type="checked"]
被选中(:checked
)时,改变 .total
的样式:
.total {
color: #c5c5c5;
font-size: 0.75em;
font-weight: 300;
transition: color 0.2s ease;
}
.card--filter:has(input[type="checkbox"]:checked) .total {
color: #9739e8;
font-weight: bold;
}
支持 :has()
的浏览器中可以看到计数文本颜色在有选中的复选框时变成了#9739e8
:
注意,示例中的复选框按钮样式采用的是 ::before
自定义的样式,根据复选框选中与否改变计数的值使用的是 CSS 的计数器特性(counter-reset
、counter-increment
和 counter()
)。如果你对该示例中使用到的其他CSS技巧感兴趣的话,可以阅读下面这些文章:
- 伪元素能帮助我们做些什么
- 你可以用伪元素做的有趣事情
- 图解CSS:奇妙的CSS计数器世界(Part1)
- 图解CSS:奇妙的CSS计数器世界(Part2)
- 图解CSS:奇妙的CSS计数器世界(Part3)
有条件显示或隐藏表单元素
我们在设计“问卷调查”相关的表单时,有的时候会提供一个”其他“选项让用户选择,当用户选择其他选项时,将会显示一个输出框出来。也应该是说,我们可能需要根据之前的回答或选择来显示一个特定的表单字段。就如上图所示,当你选择下拉框中的”Other“选项时,会显示一个输出框,供用户输入想要的内容。
通过 :has()
选择器,我们可以检查 <select>
中的 <option>
或 input[type="radio"]
中的其他项是否选中(:checked
),如果选中,就把输入框显示出来:
.control--other {
display: none;
}
form:has(option[value="Other"]:checked) .control--other,
form:has(input[type="radio"][id="other"]:checked) .control--other {
display: block;
}
在支持:has()
的浏览器效果如下:
抽屉式菜单组件
抽屉式菜单有一个专业名称,即 Off-screen navigation!
抽屉式菜单的效果,曾在《CSS :focus-within
》中使用:focus-within
来实现:
我们可以通过 :has()
选择器来实现:
#nav-container:has(input[type="checkbox"]:checked) .bg {
visibility: visible;
opacity: 0.6;
}
#nav-container:has(input[type="checkbox"]:checked) .icon-bar:nth-of-type(1) {
transform: translate3d(0, 8px, 0) rotate(45deg);
}
#nav-container:has(input[type="checkbox"]:checked) .icon-bar:nth-of-type(2) {
opacity: 0;
}
#nav-container:has(input[type="checkbox"]:checked) .icon-bar:nth-of-type(3) {
transform: translate3d(0, -8px, 0) rotate(-45deg);
}
#nav-container:has(input[type="checkbox"]:checked) #nav-content {
transform: none;
}
详细代码请参阅下面这个 Codepen:
效果如下:
悬浮效果
这几个示例向大家展示的是通过 :has()
来判断元素是否选中 :checked
,从而改变 UI 效果或交互效果。我们再来看两个关于 :hover
效果的示例。
比如上面这个示例效果,用户鼠标悬浮在图片上时(当前图片具有:hover
状态),其他非悬浮状态的图片(非:hover
)具有与鼠标当前所处的图片效果不一样,比如说置灰,模糊等。
以往我们可以使用 :not()
和 :hover
来实现,比如:
.figures:hover figure:not(:hover) {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), 0 6px 20px rgba(0, 0, 0, 0.15);
z-index: 1;
position: relative;
background: inherit;
transition: all 0.2s ease;
}
.figures:hover figure:not(:hover)::before {
content: "";
position: absolute;
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
backdrop-filter: blur(4px);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.18);
z-index: 2;
inset: 0;
transition: all 0.2s ease;
}
悬浮状态是一个毛玻璃的效果,如果你对这个效果实现思路和用到的CSS技术感兴趣的话,可以阅读《使用CSS构建Glassmorphism UI效果》一文。
如果将上面代码中的选择器换成 :has()
,可以像下面这样的写:
.figures:has(figure:hover) figure:not(:hover) {
}
.figures:has(figure:hover) figure:not(:hover)::before {
}
这种方式也可以运用于导航菜单栏上。
关键代码:
/* 当 a 链接被悬停时,改变列表 ul 边框样式 */
ul:has(a:hover) {
border: 2px solid whitesmoke;
}
/* 当一个 a 链接被悬停时,改变其他 a 链接的样式 */
ul:has(a:hover) a:not(:hover) {
opacity: 0.5;
}
详细代码请查阅 Michelle Barker 在 Codepen 上写的示例:
在上面这个示例基础上,你还可以调给带有下拉菜单项右侧添加向下或向右指示箭头:
来看一个具体的示例(Fork 了 Håvard Brynjulfsen 在 Codepen示例)。在 Håvard Brynjulfsen 的示例基础添加了 :has()
选择器:
.menu:has(button:hover) {
border: 2px solid #d32c2c;
}
.menu-list:has(button:hover) button:not(:hover){
filter: opacity(0.5) blur(1px);
}
.menu-item:has(button + ul) > button::after {
content: "➤";
display: flex;
align-items: center;
margin-left: auto;
padding-right: 32px;
height: 100%;
top: 0;
position: absolute;
right: -22px;
}
支持的浏览器中将看到下面这样的效果:
基于验证的变化
在 《美化表单的CSS高级技巧》和 《初探CSS 选择器Level 4》两篇文章中介绍了可运用于表单上的伪类,在 CSS 中把它们称为 表单伪类选择器,比如前面提到的 :checked
,:disabled
,除此之外还有:
:enabled
和:disabled
:read-only
和:read-write
:placeholder-shown
:default
:indeterminate
:valid
和:invalid
:required
和:optional
:in-range
和:out-of-range
正如文章所介绍的示例,我们可以使用这些伪类,结合 CSS 的 :not()
选择器,相邻兄弟选择器 +
和通用兄弟选择器来设计一个带有不同验证样式的表单。比如:
input:required + .help-text::before {
content: '*Required';
}
input:optional + .help-text::before {
content: '*Optional';
}
input:read-only {
border-color: var(--gray-lighter) !important;
color: var(--gray);
cursor: not-allowed;
}
input:valid {
border-color: var(--color-primary);
background-image: url("right.svg");
}
input:invalid {
border-color: var(--color-error);
background-image: url("error.svg");
}
input:invalid:focus {
border-color: var(--color-error);
}
input:invalid + .help-text {
color: var(--color-error);
}
input[type='email']:invalid + .help-text::before {
content: 'You must enter a valid email.'
}
input:out-of-range + .help-text::before {
content: 'Out of range';
}
input[type='checkbox'] + label {
user-select: none;
}
看上很完美。但它们的组合使用有一个缺陷。我们无法使用他们选中元素的前面元素或其祖先元素。比如,我们要实现下面这样的效果:
你可能想到了。如果想通过其他选择器来选中排在前面的元素或其祖先元素,那么就需要用到今天所说的:has()
关系选择器。也就是说,将:has()
、:not()
、表单伪类选择器以及相邻兄弟(+
)和相邻兄弟通用选择器(~
)结合在一起,我们可做的事情会更多,灵活性也会更大。
比如下面这个示例:
使用了 :has()
选择器的关键代码:
/* 选中”其他“选项时,对应的输入框显式 */
form:has(option[value="other"]:checked) .control--other-job,
form:has(input[type="checkbox"][value="other"]:checked) .control--other-interests {
display: block;
}
/* 带有 required 属性的input 对应的 lable 前添加 "❋" 提示符及样式设置 */
.control:has(input:required) label::before {
content: "❋";
font-size: 1em;
font-weight: bolder;
color: #ff0092;
}
/* 未含 required 属性的input 对应的 lable 前添加 "⇟" 提示符及样式设置 */
.control:has(input:optional, select:optional) > label:not(input + label)::before,
.control:has(input:optional, select:optional) > span::before {
content: "⇟";
font-size: 1em;
font-weight: bold;
color: #93ff00;
}
/* 输入无效值,改变输入框前标签元素文样颜色 */
.control:has(input:invalid) label:not(input + label) {
color: #ff3e46;
}
/* 输入无效值,改变输入框样式 */
.control:has(input:invalid) input {
border-color: #ff3e46;
box-shadow: inset 0 -5px 45px rgb(207 47 127 / 20%),
0 1px 1px rgb(216 20 111 / 20%);
border-color: #ff008e;
color: #ff008e;
}
/* 输入无效值时,提示信息显示*/
.control:has(input:invalid) .error {
display: block;
}
/* 输入有效值,改变输入框前面标签元素的文本颜色 */
.control:has(input:valid) label:not(input + label) {
color: #0be498;
}
/* 输入无效值,警告提示信息显示,输入框输入有效值,警告提示信息框隐藏 */
.form:has(input:invalid) .alert__error {
display: flex;
}
详细代码请参阅下面这个 Demo:
支持 :has()
的浏览器你将看到的效果如下:
文章中提到的案例仅是 :has()
关系型选择器部分运用场景,也只是:has()
选择器使用的开始。你可以把你日常碰到的需要使用关系型选择器(或者说父选择器)场景罗列出来,尝试着使用它。或者发挥你的才智,使用:has()
创造更多的案例。我相信这样创作的过程中,你肯定能发现:has()
有很多用途和作用。
小结
在这篇文章中,我们主要和大家一起探讨了关系型选择器:has()
是什么,能解决什么问题,并且给大家提供了一些使用 :has()
选择器的案例和场景。
其实,关系型选择器:has()
和 CSS容器查询 特性很相似,它能在 CSS 中带来一定的逻辑关系。它的到来,将允许开发者编写强大的,多功能的选择器,而这些选择器目前在 CSS 中是无法实现的,也是开发者期待已久的功能。
今天,如果选择器依赖于子元素的状态,那么开发者通过编写多个需要手动应用或使用 JavaScript 修改 CSS 类来处理缺少的父选择器功能。关系型选择器:has()
允许开发者编写自我记录的强大选择器,从而减少CSS的类名的使用以及减少JavaScript的依赖(比如示例中的动态改变样式)。
我一直追求的原则:能使用CSS解决的问题绝不使用JavaScript!