初探CSS 选择器Level 4

发布于 大漠

在开始聊CSS选择器Level 4(Selectors Level 4)之前,先要明确一个简单的概念。时至今日,在CSS的世界之中再不会有大版本的称谓,比如以前大家熟悉的CSS2.1、CSS3。现在的CSS都只会以CSS模块的版本来进行区分,比如我们今天要聊的CSS选择器,其最新版本就是:CSS Selectors Level 4。在CSS Selectors Level 4中为选择器增添了不少的新特性,当然这些新特性有的已得到浏览器的支持,有的只得到部分主流浏览器的支持。所以接下来,花点时间学习一下,以备后用。

:not()

否定伪类:not()是一个函数伪类,以选择器列表做为参数,它表示的元素不是由其参数表示的。:not()选择器可以用来做为判断的一个选择器,好比JavaScript中的非。其主要作用就是将符合规则的元素剔除,将样式规则应用于其他元素上。事实上,在CSS Selector Level 3就有:not()的身影,只不过当初的功能比较弱,比如:not(p)用来选择不是<p>的元素。但在新版本的中,其功能变得更为强大,可以应用更为复杂的规则,但是同样地不允许嵌套使用,比如:not(:not(...))

我们平时开发项目的时候,时常会碰到列表这样的效果,列表项之间有一个margin-bottom,而往往想在最后一项中不设置margin-bottom。比如像下图这样的效果:

往往我们借助伪类选择器:last-child来帮我实现,比如:

li{
    margin-bottom: 20px;
}

li:last-child {
    margin-bottom: 0;
}

如果我们使用:not()选择器,会变得更容易:

li:not(:last-child) {
    margin-bottom: 20px
}

上面的代码表示的意思就是:选中除最后一项li的所有li,并给其设置margin-bottom: 20px;

另外,:not()选择器还有一个提高CSS权重的小作用,比如div:not(span)div是同一个概念,但是明显的前者的优先级要更高。

浏览器的支持度:

:has()

关系伪类:has():not()选择器类似,也是一个函数伪类,不同的是:has()使用相对选择器列表作为参数。它表示一个元素,任何一个相对选择器(当将元素绝对化并将其赋值为:scope元素)至少匹配一个元素。

:has()伪类有点类似于jQuery中的.has()方法,简单来说就是用来匹配含有某些规则的元素。比如:

// 匹配含有`img`子元素的`a`元素
a:has(> img) { }

// 匹配拥有dt兄弟元素的dt元素
dt:has(+ dt) {}

// 匹配不含有h1、h2、h3、h4、h5和h6的section元素
section:not(:has(h1, h2, h3, h4, h5, h6)) {}

// 匹配含有的不是h1、h2、h3、h4、h5、h6子元素的元素
section:has(:not(h1, h2, h3, h4, h5, h6)) {}

最后两个示例是:not():has()组合在一起使用的,但两者组合在一起所达到的意思却完全不一样。其中:not(:has(selector))匹配不含有selector选择元素的元素(有点绕,对应看上面示例描述),而:has(:not(selector))匹配含有的不是selector子元素的元素。两都主要区别在于,:has(:not(selector))写法必须要含有一个子元素,而:not(:has())可以不含有元素也会被匹配。

:matches()

:matches()同样是一个函数伪类,以选择器列表作为参数。主要用于匹配所述规则的元素,并应用相应的样式规则,选择该列表中任意一个选择器可以选择的元素。它可以让我们节省书写大量的 CSS 样式匹配规则,让我们从大量重复的规则书写中解放出来。

来看两个示例:

.matches {
    color: black;
}

.matches :matches(span, div) :matches(span, div) {
    color: green;
}

上面的代码等同于:

.matches span div,
.matches span span,
.matches div span,
.matches div div {
    color: green;
}

注意,许多浏览器通过一个更旧的伪类选择器:any()来替代:matches(),并且要带上对应的浏览器私有前缀,:-webkit-any():-moz-any(),其工作方式与:matches()完全相同,比如上面示例,我们可以改写成:

.matches :-webkit-any(span, div) :-webkit-any(span, div) {
    color: green;
}
.matches :-moz-any(span, div) :-moz-any(span, div) {
    color: green;
}

来看一个简单的示例:

示例中通过一行代码:

:matches(header, main, footer) p:hover {
    color: red;
    cursor: pointer;
}

就替代了:

header p:hover,
main p:hover,
footer p:hover {
    color: red;
    cursor: pointer;
}

如果你使用过CSS的处理器,就较好理解,它有点类似于CSS处理器中的嵌套规则。

有一点需要注意,:matches()不能嵌套工作,也不能和:not()配合在一起工作,比如下面的几个选择器,都将无法正常工作:

:matches(:not(...)) {}
:not(:matches(...)) {}
:matches(:matches(...)) {}

除此之外,在:matches()传递的选择器列表中也不允许一些组合选择器,比如~>+等。

浏览器支持度:

特别声明,matches()后面将会改变:is()

:something()

:something()被称为权重调整伪类,也是一个函数伪类,其语法和功能与:matches()类似。其不同之处是:something()伪类和它的任何参数都不影响选择器的权重,它的权重总是为0

对于在选择器中引入过滤器很有用,同时保持关联的样式声明易于重写。

下面是一个常见的例子:

a:not(:hover) {
    text-decoration: none;
}

nav a {
    /* 无效果 */
    text-decoration: underline;
}

如果我们使用:something()伪类选择器就可以达到作者期望的效果:

a:something(:not(:hover)) {
    text-decoration: none;
}

nav a {
    /* Works now! */
    text-decoration: underline;
}

未来的选择器可能会引入额外的参数来显式的设置伪类的权重。

:dir()

:dir()主要用于匹配符合某个方向性的元素,例如:dir(ltr):dir(rtl),其中ltrleft to right简写,表示从左向右,而rtlright to left,表示从右向左。使用:dir()匹配元素和使用[dir="ltr"][dir="rtl"]在某种程度上是一样的效果,其区别是[dir=""]无法匹配到没有显式声明dir的元素,但:dir()却可以匹配到由浏览器计算得到或者继承来的dir属性的元素。因此,如果我们有明确地对某个元素声明dir,那么属性选择器[dir=""]就可以匹配到对应的元素,但如果我们只单纯的从父元素继承来而的dir,就需要使用伪选择器:dir()来匹配。

[dir="ltr"] {
    color: green;
    border-color: green;
}

[dir="rtl"]{
    color: #f36;
    border-color: #f36;
}

body :dir(ltr) {
    color: orange;
    border: 2px solid orange;
}

支持的浏览器下看到的效果如下图:

浏览器支持度:

注意,dir是HTML元素的属性,对应的效果和CSS的direction属性一样。

:lang()

很多Web页面都会在html元素上显式的指定lang属性,比如小站默认指定的是zh-hans,如下图所示:

:lang()就可以用来匹配声明了lang="val"的元素,并且可以使用通配符匹配,例如html:lang(*-CH)将可以匹配de-CHhtml元素。

html:lang(de-DE) {
    color: green;
}
html:lang(*-CH) {
    color: blue;
}

:any-link:local-link

:any-link用于匹配带有href属性的元素,比如常见的<a><link>等元素。如果元素匹配:link:visited,则它匹配元素,并且与:matches(:link, :visited)等价。比如下面这个示例:

:link, :visited {
    color: #f36;
}

:matches(:link, :visited) {
    color: #f36;
}

也可以用:any-link来写:

:any-link {
    color: #f36;
}

:-webkit-any-link {
    color: #f36;
}

:-moz-any-link {
    color: #f36;
}

其中:-webkit-any-link:-moz-any-link是其兼容性写法。该选择器的作用在于可以选出所有带有链接的元素,如果使用旧方法,那么只能使用标签名的方式或者 a[href="value"] 的方式去匹配。

:local-link伪类允许作者根据用户在站点中的当前位置对超链接进行样式设置。它表示一个元素,该元素是超链接的锚点来源,其目标的绝对URL与该元素自己的文档URL相匹配。如果超链接的目标包含一个片段URL,那么当前URL的片段URL也必须匹配;如果没有,则在比较中不考虑当前URL的片段URL部分。

例如,以下规则防止导航菜单指向当前页面的链接添加下划线:

nav :local-link { text-decoration: none; } 

注意,页面当前URL可能会因为用户的操作而改变,比如激活一个链接,目标是同一页面中不同的片段,或使用pushState API;还有一些更明显的操作,如导航到不同的页面或遵循重定向(可以由HTTP等协议发起),如<meta http-equiv="...">或脚本指令。UAs必须确保:local-link以及以下伪类中的:target:target-within在状态中正确响应所有此类更改。

:target:target-within

在某些文档语言中,文档的URL可以通过URL的片段进一步指向文档中的特定元素。以这种方式指向的元素是文档的目标元素。

其中片段标识符是URL中紧跟#的部分,例如#top#footnote1。你可能已经使用它们创建页面内导航,比如大家常见的“跳转链接”。有了:target伪类,我们可以突出显示与该片段对应的文档部分,而且无需JavaScript也可以做到这一点。

借助:target强大的特性,我们可以使用该伪类实现一些JavaScript的功能。比如TabAccordionModal的制作。

具体的示例代码不在这里贴上了,感兴趣的可以看看在线示例。

Accordion效果:

Tab效果:

Modal效果:

:target-within伪类应用于使用了:target伪类的元素。

:scope

在某些上下文中,选择器可以与一组显式的:scope元素匹配。这是一组(可能是空的)元素,它们为选择器提供了一个参考点,以便与之匹配。例如DOM中的querySelector()调用指定的引用点。

:scope伪类表示作为:scope元素的任何元素。如果:scope元素没有显式指定,但是选择器有作用域,其范围根是一个元素,那么:scope就代表作用域根;就相当于:root。用于匹配特定元素而不是文档根元素的这个伪类的规范必须定义一个作用域根(如查使用作用域选择器)或一组显示的:scope元素。

<div class="scope">
    <p>This paragraph is outside the scope.</p>
    <div>
        <style scoped>
            :scope {
                background-color: red;
            }
            p {
                color: blue;
            }
        </style>
        <p>This paragraph is inside the scope.</p>
    </div>
</div>

以上代码,第二个 div 将会有红色背景,并且他的所有 <p> 子元素都将拥有蓝色文字。

:current():past():future()

:current(), :past(), :future()这三个伪类有一个统一的称呼:时间轴伪类:current() 匹配时间轴当前的元素,:past() 匹配 :current()元素之前的元素,:future() 则匹配当前时间轴后的所有元素。这里说的时间轴指的是例如 WebVTT

值得注意的是,规范中写道如果使用的时间轴并不是文档语言所规定的,那么 :past():future() 有可能分别匹配 :current() 元素的前面的兄弟元素和后面的兄弟元素。由于在 Chrome Canary 和 Safari TP 上都不支持这几个伪类,所以无法实验正确性。下面使用的例子是从这个网址摘过来的。

:current(p, span) {
    background-color: yellow;
}

:past(p, span),
:future(p, span) {
    background-color: gray;
}

<video controls preload="metadata">
    <source src="//html5demos.com/assets/dizzy.mp4" type="video/mp4" />
    <source src="//html5demos.com/assets/dizzy.webm" type="video/webm" />
    <source src="//html5demos.com/assets/dizzy.ogv" type="video/ogv" />

    <track label="English" kind="subtitles" srclang="en" src="//www.iandevlin.com/html5test/webvtt/upc-video-subtitles-en.vtt" default>
</video>

表单伪类

这一节中的伪类主要应用于接受用户输入的元素,比如input元素。

:enabled:disabled

在HTML中,有些表单input元素有可用(enabled)和不可用(disabled)这两种状态,而在CSS中我们可以使用:enabled伪类选择器和:disabled伪类选择器来分别设置这些可用和不可用input元素的样式。

CSS 伪类 :enabled 表示任何启用的(enabled)元素。如果一个元素能够被激活(如选择、点击或接受文本输入)或获取焦点,则该元素是启用的。元素还有一个禁用的状态(disabled state),在被禁用时,元素不能被激活或获取焦点。

input:not([type="checkbox"]):enabled {
    border: 2px solid green;
}

CSS 伪类 :disabled 表示任何被禁用的元素。如果一个元素不能被激活(如选择、点击或接受文本输入)或获取焦点,则该元素处于被禁用状态。元素还有一个启用状态(enabled state),在启用状态下,元素可以被激活或获取焦点。

input:not([type="checkbox"]):disabled {
    border: 2px solid red;
}

:read-only:read-write

任何可编辑的元素都是read-write状态,反之,元素则是read-only状态。:read-only伪类匹配不可被编辑的元素,:read-write伪类则匹配可被编辑的元素,例如 <input> 或者 contenteditable="true" 的元素。:-moz-read-only:-moz-read-write 分别是他们的兼容性写法。

input:read-only {
    border: solid 2px blue;
}
div[contenteditable]:read-write,
input:read-write {
    border: solid 2px red;
}

浏览器支持度:

:placeholder-shown

:placeholder-shown 匹配 placeholder 文字显示时的 <input> 元素。::-webkit-input-placeholder, ::-moz-placeholder, :-ms-input-placeholder 分别是它在不同浏览器的兼容性写法。在此之前,原生的 placeholder 文字是没有方法去改变其颜色的,大多数做法是使用 value 来代替 placeholder,同时利用 JavaScript 对 inputfocus 事件进行监听,将 value 清空,从而达到一个模仿 placeholder 的效果。

#email:placeholder-shown + #submit:not(:focus) {
    opacity: .5;
    pointer-events: none;
}

:default

:default匹配一组相似元素集合中的默认元素,例如 <form> 中有多个 <input>,其中有一个是 <input type="submit">,那么该元素将会被匹配。此外还有 <option> 也有默认元素。

input:default {
    border: 2px solid red;
}

浏览器支持度:

:indeterminate

radiocheckbox 元素上一般有两种状态:选中未选中,但是有的时候的状态会是不确定状态,而 :indeterminate就是匹配这种不确定状态的 radiocheckbox

.indeterminate {
    label { 
        background: #429032; 
    }

    :indeterminate + label {
        background: #c03;
    }
}

通常地,radiocheckbox 在没有声明选中状态时,默认只有两种可能性:checkedunchecked,为了让他们出现第三种状态,我们可以借助 JavaScript 来控制:

document.getElementsByClassName("indeterminate-choice")[0].indeterminate = true;

上面例子的 <label><input> 处于 indeterminate状态的时候,labelbackground-color将会变为#c03

浏览器支持度:

:valid:invalid

:valid可以让用户得知字段输入正确。比如,在 <input type="email"> 中,如果我们输入了 123,那么此时 :invalid 将会匹配该元素,假如我们输入 w3cplus@hotmail.com,那么此时 :valid 将会匹配该元素。这里要注意假如我们没有为 <input> 作约束,例如 <input type="text">,那么它的任意输入将使元素既不会被 :valid 匹配,也不会被 :invalid 匹配。

input[type=email]:valid { 
    border-color: #429032; 
}
input[type=email]:invalid { 
    border-color: #c03; 
}

浏览器支持度:

:required:optional

:required:optional 分别匹配带有 required 标识的元素和不带 required 标识的元素。同样地,我们可以利用这两个伪类来对需要填写的元素添加特定的样式。

input:required ~ .msg:after {
    content: '*';
    color: red;
}

input:optional ~ .msg:after {
    content: '(optional)';
}

浏览器支持度:

:in-range:out-of-range

:in-range:out-of-range 只对有被条件约束的元素起作用,例如 <input type="number" min="1" value="1">,如果输入数字小于 1,那么将会被 :out-of-range 匹配,反之则是被 :in-range 匹配。在很多时候,我们需要对“脏值”做一个高亮的显示,以前可能需要配合 JavaScript 对值的边界进行检测,然后在对元素的样式进行修改。而现在,有了这两个伪类的存在,我们可以完全使用 CSS 来控制。

//:in-range
.in-range {
    input[type=number] {
        border: 5px solid $r;
    }    
    input[type=number]:in-range {
        border: 5px solid $g;
    }    
}

//:out-of-range
.out-of-range {
    input[type=number] {
        border: 5px solid $g;
    } 
    input[type=number]:out-of-range {
        border: 5px solid $r;
    }
}

浏览器支持度:

:user-error

:user-error 会匹配 :invalid, :out-of-range 和没有任何值的 :required 元素,但是假如是初始化时就触发这三种错误,:user-error 将不会匹配该元素,只有当用户和元素进行交互或者提交了该表单并且触发了这三种错误,:user-error 才会被触发。Chrome 和 Safari 可能尚未支持,所以无法验证正确性。

.user-error input:user-error {
    color: red;
}

.user-error input:valid {
    color: green;
}

<div class="user-error">
    <input type="email" name="eamil_valid" value="abc@abc.com">
    <input type="email" name="email_invalid" value="abc">
</div>

这些伪类以及HTML自带的一些属性,可以更轻易的帮助我们做表单验证和美化单化。如果你对这方面的知识感兴趣的话,可以阅读下面几篇文章:

:focus-within

在CSS中:focus-within是一个伪类,他就像你知道的:focus或者:hover:focus-within能非常方便处理获取焦点状态。当元素本身或其后代元素获得焦点时,:focus-within伪类的元素就会有效。

#nav-container:focus-within .bg {
    visibility: visible;
    opacity: .6;
}
#nav-container:focus-within .button {
    pointer-events: none;
}
#nav-container:focus-within .icon-bar:nth-of-type(1) {
    transform: translate3d(0,8px,0) rotate(45deg);
}
#nav-container:focus-within .icon-bar:nth-of-type(2) {
    opacity: 0;
}
#nav-container:focus-within .icon-bar:nth-of-type(3) {
    transform: translate3d(0,-8px,0) rotate(-45deg);
}

浏览器支持度:

有关于:focus-within更详细的介绍,可以移步阅读早前整理的一篇专文《CSS :focus-within》。

:drop:drop()

:drop:drop() 匹配可被放置拖动元素的目标元素,两者区别在于 :drop() 可以匹配一些规则,包括 active, valid, invalid

  • active:为被拖动的元素显示当前的放置目标
  • valid:显示与被拖动元素相关联的放置目标是否有效
  • invalid:和前一个相反,如果与被拖动元素相关联的放置目标无效则为其应用样式

如果 :drop() 括号里没有任何过滤,那么将和 :drop 没有区别。

网格结构选择器

该特性将对例如 <table> 的栅格布局起作用。它包含 :column(selector), :nth-column(n):nth-last-column(n)。目前浏览器都还未支持,无法实验正确性。这些伪类将让栅格布局的样式控制变得更为简单,不过更多的试验要等到浏览器支持才能一一试验。

  • :column(selector): 将匹配例如 <table> 中 带有 selector 类名的那一列的所有元素
  • :nth-column(n):匹配括号内 n 的计算值的某一列的元素,计算方式是从头开始计算
  • :nth-last-column(n):匹配括号内 n 的计算值的某一列的元素,计算方式是从后开始计算

:empty:blank

前两天专门为:empty:blank写了一文章。简单的说,:empty:blank 都是CSS的伪选择器。区别在于 :empty 只能匹配没有任何内容的元素,而 :blank可以匹配带有 spaces(空格), tabs(缩进符) 和 segment breaks(段落过段) 内容的元素。

总结

文章介绍了目前 CSS Selectors Level 4 的一些新的特性,我们看到 CSS 正在逐渐将以前需要依赖 JavaScript 做到的事情转化为 CSS 自身能够处理的过程,这个将大大降低了 CSS 和 JavaScript 之间的代码耦合,从而也降低了项目迭代过程中的维护成本。文章中涉及到的很多CSS选择器不一定现在就用得上,但随着技术往前的推移,这些特性将离我们越来越近。我想我们很快就能用得上。特别是表单验证方面的特性,其实大家在写表单的时候就可以运用上。

文章内容有点偏长,如果文章中有不对之处,欢迎在下面的评论中指正,如果你有这方面其他的使用经验或使用用例,欢迎在下面的评论中与我们一起分享。

参考资料

CSS选择器相关教程