CSS 选择器:is() 和 :where() 与 :has() 有什么功能
对于 CSSer 而言,CSS 选择器是 CSS 领域最基础不过的知识了,他虽基础,但在给元素设置样式,选择器是不可或缺的。正所谓,“众里寻他千百度,蓦然回首,那人却在灯火阑珊处”!在 Web 开发中,我们需要通过选择器的能力,在众人中找到想找到的人。话又说回来,选择器虽然简单,但它却很重要,就从 W3C 规范中有关于选择器版本的迭代中也能说明这一点(现在已经是 Level 4 版本了)。事实上,早在2018年年底我就在《初探CSS 选择器Level 4》一文中和大家一起探讨了在选择器 Level 4 中新增的选择器。
可以肯定的是,在那个时候,大部分同学都认为,这一切太早了,还不知道什么时候能得到主流浏览器支持呢?看上去是没啥问题,但对于我而言,我一直希望自己能把 CSS 方面最新的特性带给大家。暂时抛开新不说,回到CSS选择器来说,就在小站,从最初开始写博客,这部分就是重要内容之一。到今天,可以在小站上看到关于 CSS 选择器 方面的内容已有多篇:
- CSS选择器之基本选择器
- CSS选择器之属性选择器
- CSS选择器之伪类选择器
- CSS选择器的优化
- 使用 CSS Mod Queries 控制选择器范围
- CSS伪选择器:
:empty
vs.:blank
- 再聊CSS的属性选择器
- 初探CSS选择器 Level 4
- 编写高效CSS选择器
我一直在重构《图解CSS》中有关于选择器的内容,只因其更新迭代太快,为此一直延迟到今天还未完成。
你可能已经从标题中预判到了,接下来要和大家聊的是 CSS 选择器中的 :is()
、:has()
和 :where()
三个伪类选择器。这三个选择器也是今年(虽然2021年马上就要结束了)最为热门的、讨论最多的选择器特性,甚至可以说是大家一直以来期待的选择器。如果你阅读过《初探CSS 选择器Level 4》一文,我想你对 :is()
和 :has()
会略有了解。如果你从未接触过,也不要紧。接下来,将和大家一起聊聊这三个选择器,因为这三个选择器真的很有意思。那么接下来,我们就一起来聊聊这三个选择器有何功能,能帮我解决什么样的问题?
:is()
首先要说的是, :is()
、:has()
和 :where()
选择器都隶属于 CSS 选择器 Level 4版本中,它们虽称为选择器,但也被称为 CSS 伪类函数,也可以纳入到 CSS 函数的体系中 。我们先从:is()
选择器开始!
:is()
选择器它取替了早期的:any()
和:matches()
选择器!因此也被称为 “匹配任何” 的伪类选择器。
CSS 的 :is()
选择器(伪类函数)接受一个选择器列表作为其参数,即,它可以接受一个选择器的列表来尝试匹配。这样一来,开发者就可以以更紧凑的形式编写大型选择器。通过一个简单地示例来帮助大家理解。
在编写 CSS 时,有时可能会使用组合选择器(可能是很长的选择器列来)来选择具有相同样式规则的多个元素。如:
button.focus,
button:focus {
/* CSS Code */
}
article > h1,
article > h2,
article > h3,
article > h4,
article > h5,
article > h6 {
/* CSS Code */
}
上面的选择器,使用 :is()
选择器的话,可以变成:
button:is(.focus, :focus) {
/* CSS Code */
}
article > :is(h1, h2, h3, h4, h5, h6) {
/* CSS Code */
}
是不是简化很多了。
上面示例所演示的只是 :is()
选择器最基础的使用,但它有几个重要的事实,即 :is()
伪类函数中的列表参数有几个独特行为:
:is()
伪类函数的列表值是宽松的:如果列表中的选择器是无效的,规则将继续匹配有效的选择器:is()
选择器的权重是根据其列表来计算的:is()
伪类函数的列表值中不能使用伪元素(至少到目前为止是这样):is()
伪函数的成组
:is()
伪函数的参数(列表值)是宽容的
:is()
伪函数对于其参数(即一个选择在列表)是很友好,很宽容的。假设你手误在 :is()
伪函数中输入一个无效的选择器,比如:
:is(-ua-invalid, article, p, $css:rocks) {
color: hotpink;
}
熟悉 CSS 选择器的同学应该都能一眼看出,列表参数中-ua-invalid
和 $css:rocks
是个无效的选择器。在不使用:is()
选择器时,如果直接这样书写一个选择器列表,客户端会将整个列表选择器视为一个无效选择器,相应的 article
和 p
元素也不会有任何样式(color: hotpink
不会运用到 HTML 中的 <article>
和 <p>
元素上):
庆幸的是,:is()
是一个非常友好,非常宽容的选择器,它会忽略列表选择器中无效的选择器(-un-invalid
和 $css:rocks
直接被忽略),同时保持列表中其他部分(有效的选择器)。因此,加上:is()
之后,文档中的<article>
和 <p>
元素的文本都将为会 hotpink
色:
这在少数情况下是非常有用的,比如一些必须依赖浏览器供应商前缀的选择器,而将有前缀和无前缀的选择器分组会导致规则在所有浏览器中失败。使用 :is()
选择器,你可以安全地将这些样式分组,当它们匹配时就会应用,当它们不匹配时就会忽略。
:is()
选择器权重是其根据其列表参数的权重来计算的
:is()
选择器另一个有意思的是它的权重(指的是选择器的权重)。:is()
选择器的权重是根据其参数,即列表选择器的权重来决定的。比如下这个示例:
p:is(.foo, #bar) {
color: hotpink;
}
p.foo {
color: lime;
}
最终是哪个选择器获胜呢?
我们借助Polypane的CSS选择器权重计算工具来测算每个选择器的权重。:is()
中的参数是一个选择器列表,即.foo
和#bar
,另外是在:is()
选择器之外的p.foo
,它们的权重分别是:
很明显,#bar
选择器获胜,它是一个 ID 选择器。如果将p:is(.foo, #bar)
和p.foo
两个选择器来对比:
从上图中不难发现,p:is(.foo, #bar)
获胜。咱先不说观点,你可能会感到好奇是吧。简单地来分析一下。p:is(.foo, #bar)
其实相当于 p.foo, p#bar
选择器。这样来看,p#bar
选择器权重为 (1,0,1)
,它高于p.foo
选择权重(0,1,1)
。最终获胜的是 p#bar
选择器。
回到:is()
选择器权重的计算中来,它的权重是(1,0,1)
,和 p#bar
选择器权重是等同的。这就是它的神奇之外,:is()
选择器权重是根据其参数,即列表选择器中最高权重的选择器来决定。
因此,上面示例中的代码,最终p
元素的文本颜色是hotpink
,而不是lime
。
说个题外话,如果你是一位前端面试官,这个时候你就多了一个考题了。比如,你可以像下面这样提供一个考题,即“p.foo
的文本颜色是什么?”
p {
color: black;
}
p:is(.foo, #bar) {
color: blue;
}
p:is(.foo, #bar, $this.invalid) {
color: hotpink;
}
p.foo {
color: lime;
}
如果是你,你有立马答出p.foo
的文本颜色是什么?
(^_^),我能,它的颜色是
hotpink
。
这将成为CSS的一个小技巧,比如在不实际选择任何元素的情况下增加选择器权重。比如,你想用 .button
类来选择,可以使用:is()
来给其增选择器权重:
:is(.button, #increase#specificity) {
color: hotpink;
}
.button {
color: lime;
}
即使你的HTML文档中没有任何地方出现过 ID 名: #increase
和 #specificity
同样能增加 .button
选择器权重:
最终运用于 .button
的color
是 hotpink
而不是lime
:
你是否曾经傻傻地使用
.button.button.button.button
方式来增加.button
选择器权重。
:is()
伪函数的参数列表值不能是伪元素
在 W3C 规范,可以得知,到目前为止,:is()
中的参数中不能包含伪元素:
Pseudo-elements cannot be represented by the matches-any pseudo-class; they are not valid within :is().
就拿表单中的 <input>
元素来举例吧。如<input>
的占位符文本颜色的设置,即使设置同一个颜色值,也需要分开来写:
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: pink;
}
::-moz-placeholder { /* Firefox 19+ */
color: pink;
}
:-ms-input-placeholder { /* IE 10+ */
color: pink;
}
:-moz-placeholder { /* Firefox 18- */
color: pink;
}
但我们换成下面这样写的话,选择器将被客户端忽略:
::-webkit-input-placeholder,
::-moz-placeholder,
:-ms-input-placeholder,
:-moz-placeholder {
color: pink;
}
按理说:is()
选择器可以将不符合的客户端的选择器忽略,应该可以像下面这样来写:
:is(::-webkit-input-placeholder,::-moz-placeholder,:-ms-input-placeholder, :-moz-placeholder) {
color: pink;
}
由于:is()
选择器中的参数不能包含伪元素,因此上面的选择器并未生效。
同样的,像::before
、::after
和::marker
在:is()
也会被视为无效选择器。但这并不代表着永远是这样的,说不定未来的某一天,:is()
的参数也可以包含伪元素。事实上,在Github上能找到相关的讨论,详细的可以查阅 @yisibl 姐姐发起的 《Why "Pseudo-elements cannot be represented by the matches-any pseudo-class"?》话题。
:is()
选择器的分组
我最喜欢 :is()
选择器的主要原因是,它可以对选择器进行分组,让以前的组合选择器变得简约。这也是我在应用非关键样式时不需要退步(备用)就能轻松使用的规则类型,比如:
:is(h1, h2, h3) {
line-height: 1.2;
}
:is(h2, h3):not(:first-child) {
margin-top: 2em;
}
上面的代码相当于:
h1, h2, h3 {
line-height: 1.2;
}
h2:not:(:first-child),
h3:not:(:first-child) {
margin-top: 2em;
}
就上面的示例而言,对于不支持:is()
的浏览器而言,从基本样式中继承更大的line-height
或没有margin-top
并不是一个问题。它们仅仅是不理想。你还不想使用 :is()
选择器,主要还是在一些关键样式的地方,比如布局中的 Grid 或 Flexbox,它们可是控制整个页面布局的样式,如果浏览器不支持,就很可能会造成页面的混乱。
抛开浏览器对 :is()
选择器的兼容性不说。就聊:is()
的分组相关的事情。简单地说,:is()
可以做任何关于分组的事情。这包括在任何地方,任何方式的使用(比如,选择器的嵌套和堆叠)。比如:
article :is(header, footer) > p {
color: gray;
}
:is(.dark-theme, .dim-theme) :is(button, a) {
color: rebeccapurple;
}
:is(h1, h2):is(.hero, .subtitle) {
color: lime;
}
.hero:is(h1, h2, :is(.header, .boldest)) {
color: #09f;
}
一眼看上去,并不知道它们所代表的含义吧。把上面的代码换作非为:is()
选择器的写法,就是:
article header > p,
article footer > p {
color: gray;
}
.dark-theme button,
.dark-theme a,
.dim-theme button,
.dim-theme a {
color: rebeccapurple;
}
h1.hero,
h1.subtitle,
h2.hero,
h2.subtitle {
color: lime;
}
h1.hero,
h2.hero,
.hero.header,
.hero.boldest {
color: #09f;
}
从这一点来说,使用 :is()
还是有一定成本的,至少理解成本蛮大的。如果你要使用 :is()
选择器,那得想清楚或者找到可以从:is()
中受益的地方,并不是说,一律使用:is()
就是有益的。
换句话说,简单和复杂选择器与:is()
的结合是不一样的。比如下面这几个简单和复杂选择器的例子,它可以说明这一点:
p:is(article > *) {
color: #f36;
}
article > :is(p,blockquote) {
color: black;
}
:is(.dark-theme.hero > h1) {
color: lime;
}
上面的代码相当于:
article > p {
color: #f36;
}
article > p,
article > blockquote {
color: black;
}
.dark-theme.hero > h1{
color: lime;
}
从我自己体验来讲,虽然 :is()
能让我们简化选择器的使用,但不建议使用过于复杂的选择器组,这会让使用和阅读你代码的同学难以理解,甚至时间久了,自己都有可能无法理解其真正意图和作用。
最后值得一提的是,:is()
选择器自身是一个较新选择器之外,相应的 @supports
也有新的版本,即 @supports selector
。这也可以作为 @supports not selector
使用。我们可以使用下面的方法来检测 :is()
选择器是否得到浏览器支持:
@supports selector(:is(h1)) {
:is(h1, h2, h3) {
line-height: 1.1;
}
}
如果用一句话来描述 :is()
选择器主要功能的话,可以这样来描述: :is()
选择器主要功能是使原本相当冗长、复杂和容易出错的选择器更容易编写。而增加选择器权重只是使用该选择器的一个小技巧!
:where()
:where()
选择器和 :is()
选择器几乎相同,唯一不同之处是:
:where()
选择器权重永远为0
。
比如:
:where(.foo, #bar) {
/* CSS Code */
}
:where(p.foo, #bar, p#bar, $css:rocks) {
/* CSS Code */
}
:where(p.foo, .foo, p#bar, #bar, #foo#bar) {
/* CSS Code */
}
不管 :where()
选择器中参数(一个列表选择器)中的选择器权重是多大,最终:where()
的权重都是 0
:
这对那些正在建立框架、主题和设计系统的人来说非常有益。使用 :where()
可以让选择器的权重为0
,而下游的开发者可以轻易地覆盖或扩展,而不需再担心因选择器权重产生冲突。这一特性,我已经在 @argyleink 的 open-props 看到了:
:where(html) {
--ease-1: cubic-bezier(.25, 0, .5, 1);
--ease-2: cubic-bezier(.25, 0, .4, 1);
--ease-3: cubic-bezier(.25, 0, .3, 1);
--ease-4: cubic-bezier(.25, 0, .2, 1);
}
:where(html)
选择器比:root
和html
选择器权重都要低:
另外,:where()
用在重置的CSS中也非常有益,比如《聊聊重置 CSS 那些事儿》文中提到的 @Elad 的最新重置CSS中就有 :where()
选择器的身影:
:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
all: unset;
display: revert;
}
我们来看一个 :where()
的示例,比如下面这个运用于<img>
的样式,你认为图片边框是会是什么颜色:
:where(article img:not(:first-child)) {
border: 5px solid red;
}
:where(article) img {
border: 5px solid green;
}
img {
border: 5px solid orange;
}
第一条规则的选择器权重为(0,0,0)
,因为整个选择器都被包含在:where()
中;第二条规则的选择器权重是 (0,0,1)
,其中img
不在:where()
选择器中;第三条规则的选择器权重是(0,0,1)
:
就这个示例而言,第二条规则和第三条规则的选择器权重是相等的,但第三条规则位于第二条规则之后,因此第三条规则的获胜,所以运用到 img
的边框颜色是orange
,而不是green
:
:has()
拿 @SaraSoueidan 在 Twitter 上引用 @Jen Simmons 的话来说: :has()
选择器本质上就是 CSS 中期待已久的父选择器!
CSS 选择器 Level 4 的规范是这样描述 :has()
选择器:
:has()
伪类代表一个元素,如果作为参数传递的任何选择器至少与一个元素相匹配。
众所周知,CSS 的 父选择器 和 容器查询 的功能一直以来都是前端开发者最期待和最喜欢的功能。这一点从刚发布的 2021 年 CSS发展状态的相关报告中就能看得出来:
庆幸的是,这两个一直以来被期待的功能离我们越来越近了。**CSS 容器查询**功能虽然还只是一个实验性功能,但在一些主流浏览器上都已经可以体验了,具体的可阅读:
就在这几天,新发布的 Safari Technology Preview 137 浏览器已经默认开启了 :has()
选择器。
CSS的父选择器和容器查询特性能来到我们的身边,我们都需要感谢 Igalia 的团队,这个团队已经开发了一些有名的 Web 引擎的功能,比如 CSS 容器查询、 CSS 网格,以及
:has()
选择器的原型等。@Byungwoo Lee也在他的博文《CSS Selectors:has()
》中详细阐述过:has()
选择器。
既然如此,让我们深入了解一下 :has()
选择器。
前面提到过,:has()
选择器被戏称为 “父选择器”,因为默认情况下确实允许你选择一个拥有某些子元素的父元素。它的工作逻辑类似于:focus-within
和:is()
与后代选择器的组合,在这里,你正在寻找后代元素的存在,但应用的样式将是父元素。
/* 匹配包含<figcaption>后代元素的<figure>元素 */
figure:has(figcaption) {
/* CSS Code */
}
/* 匹配包含<img>子元素的<figure>元素 */
figure:has(> img) {
/* CSS Code */
}
/* 匹配包含<img>后代元素的<figure>元素 */
figure:has(img) {
/* CSS Code */
}
/* 匹配包含<img>后面有 <figcaption>元素的<figure>元素 */
figure:has(img + figcaption) {
/* CSS Code */
}
来看一个具体示例:
<!-- HTML -->
<figure>
<figcaption>CSS Pseudo-Class Specification</figcaption>
<img src="https://picsum.photos/1240/?random=11" alt="">
</figure>
<figure>
<div class="media">
<img src="https://picsum.photos/1240/?random=12" alt="">
</div>
<figcaption>CSS Pseudo-Class Specification</figcaption>
</figure>
<figure>
<img src="https://picsum.photos/1240/?random=13" alt="">
<figcaption>CSS Pseudo-Class Specification</figcaption>
</figure>
/* 匹配包含<figcaption>后代元素的<figure>元素 */
figure:has(figcaption) {
background-color: #3f51b5;
}
/* 匹配包含<img>子元素的<figure>元素 */
figure:has(> img) {
background-color: #009688;
}
/* 匹配包含<img>后代元素的<figure>元素 */
figure:has(img) {
background-color: #9c27b0;
}
/* 匹配包含<img>后面有 <figcaption>元素的<figure>元素 */
figure:has(img + figcaption) {
background-color: #607d8b;
}
下图是 Safari TP 137 (支持:has()
浏览器)和 Chrome96(目前还不支持 :has()
的浏览器)的效果:
@RockStarwind在 Twitter 分享了一些使用其他 CSS 技巧,比如
:focus-within
来模拟:has()
的案例:
使用 :has()
选择器对 DOM 的结构非常重要,DOM 树的变化将会直接影响 :has()
选择器是否能真正的匹配到相应的元素(可以使用 CSSOMTools来模拟:has()
选择器与DOM树的真实场景)。因此,:has()
选择器与其他结构类选择器,比如 :not()
选择器相似,也被称为 关系型伪类选择器。对于关系型伪类选择器有着一个共同的特征,它由以下部分组成:
<target_element>:has(<selector>) {
/* CSS Code */
}
<target_element>
:指的是 目标元素,对应 CSS 中的一个元素的选择器,如果作为参数传递给:has()
伪类的条件得到满足,该元素将成为目标元素。条件选择器的范围是这个元素<selector>
:指的是 CSS 选择器,一个用CSS选择器定义的条件,并且作为:has()
伪类选择器的参数,需要满足这个条件才能将样式应用到选择器上,即给目标元素设置样式
像大多数伪类选择器一样,传递:has()
选择器的参数<selector>
可以是和目标元素(<target_element>
)有关系的元素,比如目标元素的子元素、后代元素或相邻元素等。比下面这个示例,可以给<figure>
中的 <img>
设置不同的样式:
figure {
background-color: #3f51b5;
}
figure:has(figcaption) {
background-color: #009688;
}
figure img {
aspect-ratio: 21 / 9;
border: 5px solid #3f51b5;
}
figure:has(figcaption) img {
border: 5px solid #9c27b0;
}
支持的浏览器你将看到下图这样的效果:
另外,使用:has()
选择器,并且配合表单控制的一些属性,可以做一些交互上UI的变化,比如下面这个示例:
.button {
--button-color: hsl(0, 0%, 90%);
--button-text-color: hsl(0, 0%, 50%);
cursor: not-allowed;
}
form:has(input[type="checkbox"]:checked) .button {
--button-color: var(--color-primary);
--button-text-color: rgb(0, 25, 80);
cursor: pointer;
}
form:has(input[type="checkbox"]:checked) .button:hover {
--button-text-color: rgb(0, 25, 80);
--button-color: #2eec96e3;
}
复选框选中与否的效果如下:
我想你已经从上面的示例中感受到了:has()
选择器是多么通用、强大和有用吧。其实,它还要以和其他的伪类选择器结合起来使用,比如:not()
选择器,创建更复杂的关系选择器。比如:
.card:not(:has(*:empty)) {
/* 选择同有空元素的卡片元素 */
}
form:has(input[type="checkbox"]:not(:checked)) {
/* 选择没有选中至少一个复选框输入的表单元素 */
}
@Bramus 将:has()
、:not()
和表单控件的一些伪类选择器,在Codepen上写了一个表单方面的Demo:
不同状态的效果如下:
关系选择器并不局限于目标元素的子元素内容和状态,还可以针对 DOM 树中的相邻元素,有效的使它成为一个“上一个兄弟选择器”。:has()
也是一个关系选择器,它接受一个 <forgiving-relative-selector-list>
作为参数。那是一个<relative-selector>
的列表,它可以包含我们已经知道的任何组合器:+
、~
和 >
。
p:has(+ img) {
/* ... */
}
img:has(~figcaption:not(.hidden)) {
/* ... */
}
label:has(> input) {
/* ... */
}
简而言之,关系选择器将CSS选择锚定在一个带有:has()
伪类的元素上。下面的代码展示了 :has()
和平时选择器的差异,带有:has()
的选择器最终选择的是匹配条件的目标元素:
.card .title .icon {
/* 选中 .icon 元素 */
}
.card:has(.title .icon) {
/* 选中 .card 元素 */
}
.image + .caption {
/* 选中 .caption 元素 */
}
.image:has(+ .caption) {
/* 选中 .image 元素*/
}
小结
正如你所看到的,:is()
和 :where()
两个选择器的使用是极度相似的,唯一不同的是选择器的权重计算。:is()
选择器中权重是由传递给他的参数中选择器权重最高的一个决定,而:where()
选择器权重始终是0
。从这一点来说,:is()
可以用来增加选择器权重,:where()
可以将选择器权重置为最底。另外,这两个选择器都可以将冗长、复杂的选择器变得简单化。
:has()
选择器的出现,终于在CSS中可以让我根据相关条件来匹配父元素,因此也被戏称为父选择器,这也是开发者一直期待的 CSS 特性。它和 :not()
等选择器也被称为关系型选择器,它的结果和目标选择器以及传递给他的选择器参数有关,而且相应的 DOM 树也会最终决定选择器是否能匹配。
正如文章中的示例所示,这些选择器的到来或即将到来,他们都是用来帮助开发者解决问题。让开发者能更好,更快速的完成需求。最后,希望这篇文章能帮助你较好的理解最新的 :is()
、:where()
和 :has()
选择器以及他们的差异。