首页 > 编程笔记

CSS :is伪类用法详解

:is() 伪类可以把括号中的选择器依次分配出去,这对于复杂的有很多逗号分隔的选择器或者浏览器可能不支持的选择器非常有用。

和 :not() 伪类不同,:is() 伪类的参数从一开始就支持复杂选择器或复杂选择器列表。

例如,下面的写法都是合法的:
/* 简单选择器 */
:is(article) p {}
/* 简单选择器列表 */
:is(article, section) p {}
/* 复杂选择器 */
:is(.article[class], section) p {}
/* 带逻辑伪类的复杂选择器 */
.some-class:is(article:not([id]), section) p {}

:is() 伪类有两大作用:
1) 其一是简化选择器,比如平时开发中经常会遇到类似下面的 CSS 代码:
.cs-avatar-a > img,
.cs-avatar-b > img,
.cs-avatar-c > img,
.cs-avatar-d > img {
    display: block;
   width: 100%; height: 100%;
   border-radius: 50%;
}
此时就可以使用 :is() 伪类进行简化:
:is(.cs-avatar-a, .cs-avatar-b, .cs-avatar-c, .cs-avatar-d) > img {
   display: block;
   width: 100%; height: 100%;
   border-radius: 50%;
}

这种简化只是一维的,:is() 伪类的优势并不明显,但如果选择器是交叉组合的,:is() 伪类就大放异彩了。

例如,有序列表和无序列表可以相互嵌套,假设有两层嵌套,则最里面的 <li> 元素存在下面 4 种可能的情况:
ol ol li,
ol ul li,
ul ul li,
ul ol li {
   margin-left: 2em;
}
如果使用 :is() 伪类进行简化,则只有下面这几行代码:
:is(ol, ul) :is(ol, ul) li {
   margin-left: 2em;
}

2) 其二是可以在合并不同浏览器的私有选择器的同时,不影响浏览器的正常渲染,这个特性在需要对不同浏览器做不同处理的时候比较有用。

举个例子,:has() 伪类强大且实用,但是 Firefox 浏览器并不支持,此时,我们可能会在 Firefox 浏览器下引入一段 JavaScript 代码来兼容,然后就会有类似下面的 CSS 代码:
.container:has( > .empty) {
  height: 150px;
  display: grid;
  place-items: center;
}
/* Firefox浏览器兼容处理 */
.container.has-empty {
  height: 150px;
  display: grid;
  place-items: center;
}

支持 :has() 伪类的一段 CSS 语句不支持 :has() 伪类的另一段 CSS 规则。我想,肯定有人看到上面的 CSS 代码时会很兴奋,既然 CSS 规则都是一样的,那么把选择器合并在一起将会省去很多代码,也能方便维护。就像下面这样:
.container:has( > .empty),
.container.has-empty {
  height: 150px;
  display: grid;
  place-items: center;
}
然而事实并非如此,如果 CSS 选择器列表中出现了无效选择器且不是以 -webkit- 开头,则整个选择器会被认为是无效的,因此,上面的 CSS 代码在 Firefox 浏览器下就是无效的,因为 Firefox 浏览器不能识别 :has() 伪类。

但是有了 :is() 伪类就不一样了,例如对于下面的 CSS 代码,虽然选择器依然是合并书写的,但是此时 Firefox 浏览器认为其是合法的,也就是 :is() 伪类中的参数就算有无法解析的 CSS 选择器,也不影响其他可解析的 CSS 选择器的渲染。
/* 支持:is()伪类的浏览器均被认为合法 */
.container:is(:has( > .empty), .has-empty) {
  height: 150px;
  display: grid;
  place-items: center;
}
例如,在 Firefox 浏览器下可以看到类似下图的效果:


图 1 :is()伪类参数的合法性示意

:is()伪类在Vue等框架中的妙用

按道理讲,下面两行 CSS 语句中的选择器所匹配的元素是没有任何区别的:
.box .some-class {}
.box :is(.some-class) {}
因为此时 :is() 伪类仅仅是一个无关紧要的语法糖,既不影响选择器的优先级,也不影响匹配的规则。

如果是在常规的开发场景中,确实如此,但要是在 Vue 或者 React 等成熟的框架中,则情况就会不同,我们可以利用这种特性让我们的开发变得更顺畅。

以 Vue 框架为例,在 Vue 框架中,无论是构建模块还是组件,都会使用设置了 scoped 属性的样式,目的是让 CSS 私有,避免和外部 CSS 发生冲突。例如:
<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
}
</style>
此时框架会给类名 .logo 创建随机的属性选择器,这样可以确保 .logo 匹配的元素在当前 Vue 模块中是唯一的,如下所示:
.logo[data-v-7a7a37b1]{
    height:6em;
    padding:1.5em;
}

但是当我们需要匹配的元素是动态生成的时候(业务逻辑插入或者第三方组件),这种给类名添加随机属性选择器的特性可能会导致元素无法匹配。

例如运行下面这段 JavaScript 代码:
const html = '<img src="/vite.svg" class="logo" />'
const app = document.getElementById('app')
app.insertAdjacentHTML('afterbegin', html)
此时插入的 <img> 元素是不会被框架添加随机属性值的(如下图所示),而 <style> 元素中类名却自动添加了属性选择器,导致样式无法匹配。


图 2 插入的HTML元素没有随机属性值

如果此时希望可以匹配这个 <img> 元素,同时 CSS 代码又要写在设置了 scoped 属性的 <style> 元素中,该怎么办?除 Vue 框架内置的 :deep() 语法外,我们还可以使用 :is() 伪类。

不知是出于什么考虑,所有 :is() 伪类中的选择器在 Vue 框架中都是不会添加随机属性选择器的(:where() 伪类也有此特性),因此,我们可以利用这个特性,让设置了 scoped 属性的 <style> 元素中的 CSS 无属性匹配。例如,CSS 代码可以这样书写:
<style scoped>
* > :is(.logo) {
  height: 6em;
  padding: 1.5em;
}
</style>
此时,CSS 代码中的 .logo 选择器就是干干净净的类选择器,如图 3 所示,此时,哪怕页面中的HTML元素没有被组件添加随机属性值,也能被匹配了。


图 3 :is()伪类下的.logo类名没有属性选择器

虽然 :is() 伪类因这一特性在意想不到的地方发挥着作用,但其还是有令人遗憾的地方,尤其是不支持伪元素这一点。

:is() 伪类并不支持伪元素,例如 :is(::before, ::after) 是不合法的,这是个巨大的遗憾,因为在对浏览器原生的组件(如 range 范围选择、progress 进度、日期时间选择、颜色选择等)进行 CSS 样式自定义的时候,会出现大量的私有伪元素,例如在对原生的 type="color" 的 input 颜色输入框进行开发的时候,需要分别对 Chrome 浏览器和 Firefox 浏览器进行处理:
[type="color"]::-webkit-color-swatch {
    border: 1px solid #f7f9fa;
    border-radius: 4px;
}
[type="color"]::-moz-color-swatch {
    border: 1px solid var(--ui-light, #f7f9fa);
    border-radius: 4px;
}
可以看到,虽然 CSS 规则一样,但是由于 :is() 伪类不支持伪元素,因此选择器无法合并书写,大量的 CSS 代码无法复用:
/* 不合法 */
[type="color"]:is(::-webkit-color-swatch, ::-moz-color-swatch) {}
目前现代浏览器已经全面支持 :is() 伪类(见下表),大家可以在一些内部项目中大胆使用。

表:is()伪类的兼容性
浏览器 IE Edge Firefox Chrome Safari iOS Safari Android Browser
兼容的浏览器版本 X 12-18 X
88+ √
78+ √ 88+ √ 14+ √ 14+ √ 5-6.x (自动升级)√

推荐阅读