前端开发者学堂 - fedev.cn

CSS 选择器:is() 和 :where() 与 :has() 有什么功能

发布于 大漠

对于 CSSer 而言,CSS 选择器是 CSS 领域最基础不过的知识了,他虽基础,但在给元素设置样式,选择器是不可或缺的。正所谓,“众里寻他千百度,蓦然回首,那人却在灯火阑珊处”!在 Web 开发中,我们需要通过选择器的能力,在众人中找到想找到的人。话又说回来,选择器虽然简单,但它却很重要,就从 W3C 规范中有关于选择器版本的迭代中也能说明这一点(现在已经是 Level 4 版本了)。事实上,早在2018年年底我就在《初探CSS 选择器Level 4》一文中和大家一起探讨了在选择器 Level 4 中新增的选择器。

可以肯定的是,在那个时候,大部分同学都认为,这一切太早了,还不知道什么时候能得到主流浏览器支持呢?看上去是没啥问题,但对于我而言,我一直希望自己能把 CSS 方面最新的特性带给大家。暂时抛开新不说,回到CSS选择器来说,就在小站,从最初开始写博客,这部分就是重要内容之一。到今天,可以在小站上看到关于 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()选择器时,如果直接这样书写一个选择器列表,客户端会将整个列表选择器视为一个无效选择器,相应的 articlep 元素也不会有任何样式(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 选择器权重:

最终运用于 .buttoncolorhotpink 而不是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,而下游的开发者可以轻易地覆盖或扩展,而不需再担心因选择器权重产生冲突。这一特性,我已经在 @argyleinkopen-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)选择器比:roothtml选择器权重都要低:

另外,: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() 选择器以及他们的差异。