前言
相信对于 CSS 选择器大家都不陌生吧,但其实选择器的种类还是十分繁多的,直接抱着 W3C 的草案啃也没有什么意思。所以本文对 CSS 选择器做了一些总结,希望可以帮大家巩固一下选择器的知识,再顺便看看有没有什么你还不知道的选择器吧。
元素选择器(Elemental selectors)
元素选择器,顾名思义,就是通过 HTML 元素来进行匹配的选择器。
类型选择器(Type selectors)
类型选择器是通过 HTML 元素类型(标签)来选择的:
// 选择所有 <p> 元素
p { font-weigt: bold; }
// 选择所有 <h1> 元素
h1 { color: red; }
我们知道选择器是可以进行链式连接的,但是在每一个选择器中,类型选择器只能出现一次,且必须位于第一位,否则该选择器就匹配不到任何元素的。
通用选择器(Universal Selector)
通用选择器其实是一种特殊的类型选择器,它使用星号 *
来表示,可以匹配任意类型的 HTML 元素。所以在单独使用时,它会选择每一个元素,然后为它们指定样式:
// 将所有元素设置为红色
* { color: red; }
在没有元素选择器被选择的情况下时,通用选择器会隐式的存在:
.danger { color: red; }
// 等同于
*.danger { color: red; }
属性选择器(Attribute selectors)
属性选择器通过和 HTML 元素的属性进行匹配来选择的,它有以下的写法:
// 拥有 class 属性的元素
[class] {color: red;}
// 拥有 class 的值为 red 的元素,即 class='red'
[class='red'] {color: red;}
// 拥有 class 的值包含 red 的元素,比如 class='flex red bold'
[class~='red'] {color: red;}
// 拥有 class 的值为 red 或以 red- 作为前缀的元素,比如 class='red-text'
[class|='red'] {color: red;}
// 拥有 class 的值以 red 作为前缀的元素,比如 class='redtext'
[class^='red'] {color: red;}
// 拥有 class 的值以 red 作为后缀的元素,比如 class='textred'
[class$='red'] {color: red;}
// 拥有 class 的值包含 red 的元素,比如 class='teredxt '
[class*='red'] {color: red;}
// 属性选择器可以连续调用
[class~='red'][id='first'] {color: red;}
虽然例子中我们只使用了 class
属性,不过它可以对任何属性进行匹配,比如 id
,value
等,或者我们自定义的属性。
大小写区分
在默认情况下,选择器的大小写区分是和文档的语言相同的,比如 HTML 就不区分大小写。不过在 Selectors Level 4
草案中引入了新的标识符 i
和 s
用于手动标识是否需要区分大小写:
i
为Case-insensitive modifier
,即不区分大小写s
为Case-sensitive modifier
,即区分大小写
// 不区分大小写,即 red, RED, ReD 等都可以匹配
[class='red' i] {color: red;}
// 区分大小写,red 匹配,RED 则不匹配
[class='red' s] {color: red;}
不过需要注意的是,标识符 i
已经得到主流浏览器的支持,但是标识符 s
的支持度则不太乐观:
类选择器(Class Selector)
类选择器其实就是对于选择 class
属性时的语法糖,它使用 .red
的句法来代替 [class~='red']
这样的属性选择器:
[class~='red'] { color: red; }
// 等同于
.red { color: red; }
ID 选择器(ID Selector)
ID 选择器则是对于选择 id
属性时的语法糖,它使用 #bold
的句法来代替 [id='bold']
这样的属性选择器:
[id='bold'] { font-weight: bold; }
// 等同于
#bold { font-weight: bold; }
组合器(Combinators)
后代组合器(Descendant Combinators)
后代组合器由单个空格来表示(其实多个空格,tab,回车也行),它用于选择某一个元素中包含的元素:
<h1>This is <em>very</em> important</h1>
<h1>This is <u><em>very</em> important</u></h1>
<style>
/* 选择 h1 元素中的 em 元素 */
h1 em { color: red; }
</style>
其中后代的子元素,孙元素等都会被匹配,所以两个 very
都会被设置成红色:
子代组合器(Child Combinators)
子代组合器由 >
来表示,它和后代选择器很像,不过只会选择直接子代,而不会选择孙代等:
<h1>This is <em>very</em> important</h1>
<h1>This is <u><em>very</em> important</u></h1>
<style>
/* 选择 h1 元素中的子代 em 元素 */
h1 > em { color: red; }
</style>
只有子元素会被匹配,所以第二个 very
不会变成红色:
兄弟组合器(Subsequent-sibling Combinators)
兄弟组合器由 ~
来表示,他被用来选择某一元素的同级元素,即拥有同样父元素的另一元素:
<div>
<h1>This is h1</h1>
<h2>This is h2</h2>
<h3>This is h3</h3>
</div>
<style>
/* 选择 h1 元素的兄弟 h3 元素 */
h1 ~ h3 { color: red; }
</style>
因为 <h1>
和 <h3>
拥有同一父级元素,所以 <h3>
会被匹配:
相邻兄弟组合器(Next-sibling Combinators)
相邻兄弟组合器由 +
来表示,他被用来选择某一元素相邻的同级元素:
<div>
<h1>This is h1</h1>
<h2>This is h2</h2>
<h3>This is h3</h3>
</div>
<style>
/* 选择 h1 元素的兄弟 h3 元素 */
h1 + h3 { color: red; }
</style>
因为 <h3>
和 <h1>
并不相邻,所以不会被匹配:
伪类(Pseudo-classes)
伪类是什么呢?我们先来看一下 Selector Level 4 的定义:
Pseudo-classes are simple selectors that permit selection based on information that lies outside of the document tree or that can be awkward or impossible to express using the other simple selectors. They can also be dynamic, in the sense that an element can acquire or lose a pseudo-class while a user interacts with the document, without the document itself changing. Pseudo-classes do not appear in or modify the document source or document tree.
总的来说,伪类也是一种选择器,它可以匹配那些动态的,处于特定状态下的,或者无法被其他普通选择器匹配的元素。比如 :hover
就会在鼠标悬停时动态的匹配。
逻辑型伪类
:is()
该函数接收一列选择器,然后匹配其中所有的选择器。它可以使多个选择器结合到一起:
:is(h1, h2, h3) em:hover { color: red; }
// 等同于
h1 em:hover, h2 em:hover, h3 em:hover { color: red; }
:not()
该函数接收一列选择器,然后匹配除去它们之外剩下的元素:
<body>
<h1>This is h1</h1>
<h2>This is h2</h2>
<h3>This is h3</h3>
</body>
<style>
* { color: blue; }
:not(h1) { color: red; }
</style>
上面的例子会把除了 <h1>
以外的元素全部改为红色,效果如下:
:where()
它的用法和 :is()
一模一样,它们俩唯一的区别是 :where()
的权重始终为零:
<body>
<h1>This is h1</h1>
</body>
<style>
h1 { color:blue; }
:is(h1) { color: red; }
</style>
上面的例子中,:is()
伪类会改写同优先级的 <h1>
选择器,所以会变成红色:
<body>
<h1>This is h1</h1>
</body>
<style>
h1 { color:blue; }
:where(h1) { color: red; }
</style>
上面的例子中,我们把 :is()
改成了 :where()
,因为 :where()
伪类的优先级为零,所以它不会改写 <h1>
选择器:
交互型伪类
:hover
:hover
会在指针停留在元素上时匹配该元素。
:active
:active
会在用户激活元素匹配该元素,对于鼠标就是按下左键到松开左键的那一段时间。
:focus
:focus
会匹配获得焦点得元素,比如选中各种输入,或者通过 tab 键选中的元素。
:focus-within
:focus-within
会在该元素,或者该元素的后代获得焦点时匹配该元素:
<div class="box">
<input type="text">
</div>
<style>
.box {
padding: 20px;
}
.box:focus-within {
background-color: red;
}
</style>
比如上面的 <div>
会在其子元素被选中时触发 :focus-within
伪类。
未选中时:
选中后:
:focus-visible
:focus-visible
会在某一元素获得焦点,并且浏览器为该元素显式轮廓时匹配该元素。
是不是听上去和 :focus
也没什么区别?但其实它们还是有细微的区别的。比如说吧,在用鼠标点击 <a>
的链接时,默认是不会显示轮廓的,但是使用键盘 tab 到该链接时则会显示轮廓:
<a href ="/">To h1</a>
<style>
:focus { background-color: red; }
</style>
:focus
会在鼠标点击和键盘 tab 选中的情况下触发。
<a href ="/">To h1</a>
<style>
:focus-visible { background-color: red; }
</style>
:focus-visible
则只会在键盘 tab
选中的情况下触发。
但这个 :focus-visible
究竟有什么用呢?我们可以设想一下这个场景,在你为一个 <button>
设置了背景颜色后,在 chrome 中每次点击它时是不是会出现一个烦人的黑色轮廓?
我们可以通过加上一行:
:focus { outline: none; }
来取消其轮廓,不过这样的话,则使用键盘的人永远不知道什么时候选中了该 button
。
所以出于键盘无障碍的考虑,我们可以使用:
:not(:focus-visible) { outline: none; }
来代替。这样的话在鼠标点击时则不会再出现难看的黑色轮廓,而在键盘 tab 时依然可以实现无障碍访问。
不过需要注意的是,Safari 对于 :focus-visible
的支持仅处于 Safari Technology Preview 版本上,且各家浏览器的选中轮廓略实现方法略有不同,所以在使用时最好还是多进行一下测试。
链接型伪类
:link
:link
伪类会匹配所有还未被访问过的链接。
:visited
:visited
伪类则会匹配所有已经被访问过的链接。为了保证正确的渲染顺序,我们需要遵守 LVHA
的顺序,即按照如下顺序链式连接伪类::link —> :visited —> :hover —> :active
。
:any-link
:any-link
会选择所有链接,无论是否被访问过。它其实就是 :link
和 :visited
的集合,等同于 :is(:link, :visited)
。
:target
:target
会匹配与当前 URL 片段下的元素:
<h1 id='title'>This is h1</h1>
<a href="#title">To h1</a>
<style>
:target { color: red; }
</style>
在上面的例子中,点击了 To h1
的链接后,URL 从 index.html
变成了 index.html#title
,所以标题就会变红。
结构型伪类
:root
:root
伪类会匹配 DOM 的根元素。对于 HTML 文档来说就是 html
元素,不过 :root
优先级会比 html
元素更高。
:empty
:empty
伪类会匹配任何没有子元素的元素:
<div class="box"><!-- Hi --></div>
<div class="box">Hi</div>
<div class="box"><p>Hi</p></div>
<style>
.box {
float: left;
width: 100px;
height: 100px;
margin: 10px;
background-color: pink;
}
:empty {
background-color: red;
}
</style>
上面的例子中 :empty
只会选择第一个 <div>
,因为只有它没有子元素:
:nth-child(An + B)
该伪类会先找出当前元素的所有兄弟元素(包含自身),然后根据传入的参数,匹配其中的元素。
假如我们有这么一个列表:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
我们对其使用 :nth-child()
伪类来进行匹配的话:
// 匹配列表中的第三个元素,即 3
li:nth-child(3) { background-color: red; }
li:nth-child(0n+3) { background-color: red; }
// 匹配列表中的第单数个元素,即 2,4,6
li:nth-child(2n) { background-color: red; }
li:nth-child(even) { background-color: red; }
// 匹配列表中的第偶数个元素,即 1,3,5
li:nth-child(2n-1) { background-color: red; }
li:nth-child(odd) { background-color: red; }
// 匹配列表中的前三个元素,即 1,2,3
li:nth-child(-n+3) { background-color: red; }
但如果混入了不同元素类型的话,会发生什么呢?假设我们在 <li>
中混入了一个 <span>
,然后使用 :nth-child()
伪类:
<ul>
<li>1</li>
<li>2</li>
<span>3</span>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<style>
li:nth-child(odd) { background-color: red; }
</style>
</body>
结果如下:
我们可以看到 <span>
并没有被匹配(因为它并不是 <li>
),但是它参与了计数,所以后面的 <li>
依然能正确的被匹配。
如果想要匹配中 <span>
的话,我们可以使用后代选择器来匹配 <ul>
的子元素:
ul :nth-child(odd) { background-color: red; }
这样的话,<span>
也可以被选中了:
:nth-last-child(An + B)
该伪类和 :nth-child(An + B)
一样,只不过是从后往前进行匹配的:
// 选择列表中的倒数第三个元素
li:nth-child(3) { background-color: red; }
// 选择列表中的倒数后三个元素
li:nth-child(-n+3) { background-color: red; }
:first-child 和 :last-child
:first-child
等同于 :nth-child(1)
。即匹配兄弟元素中的第一个元素;
:last-child
等同于 :nth-last-child(1)
。即匹配兄弟元素中的最后一个元素。
:only-child
等同于 :first-child:last-child
。这意味着该元素既是第一个元素也是最后一个元素,即匹配无兄弟元素的元素。
:nth-of-type(An + B)
:nth-of-type
伪类和 :nth-child
伪类很像,只不过 :nth-of-type
会同时匹配元素类型。所以还是上面的例子:
<ul>
<li>1</li>
<li>2</li>
<span>3</span>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<style>
li:nth-of-type(odd) { background-color: red; }
</style>
</body>
结果如下:
在选择兄弟元素的时候,<span>
直接不参与计数。所以后面的 4
成为了第三个 <li>
;5
成为了第四个 <li>
,以此类推。
:nth-last-of-type(An + B)
该伪类和 :nth-of-type(An + B)
一样,只不过是从后往前进行匹配的。
:first-of-type 和 :last-of-type
:first-of-type
等同于 :nth-of-type(1)
。即匹配同类型兄弟元素中的第一个元素;
:last-of-type
等同于 :nth-last-of-type(1)
。即匹配同类型兄弟元素中的最后一个元素。
:only-of-type
等同于 :first-of-type:last-of-type
。这意味着该元素既是第一个同类型元素也是最后一个同类型元素,即匹配无同类型兄弟元素的元素。
输入型伪类
:enabled 和 :disabled
:enabled
伪类会匹配所有处于 enabled
状态下的用户交互元素。比如默认的各种输入,按钮等;
:disabled
伪类则会匹配所有处于 disabled
状态下的用户交互元素。
<input type="text" > <!-- 会被 :enabled 匹配 -->
<input type="date" disabled > <!-- 会被 :disabled 匹配 -->
<button>button</button> <!-- 会被 :enabled 匹配 -->
<button disabled >button</button> <!-- 会被 :disabled 匹配 -->
<textarea></textarea> <!-- 会被 :enabled 匹配 -->
<textarea disabled ></textarea> <!-- 会被 :disabled 匹配 -->
:read-write 和 :read-only
:read-write
伪类会匹配所有可以被用户编辑的的元素。比如各种 <input>
,<textarea>
以及拥有 contentditable
属性的元素;
而 :read-only
伪类会匹配所有不可以被用户编辑的的元素。即除去 :read-write
之外的所有元素,等同于 :not(:read-write)
。
<input type="text"> <!-- 会被 :read-write 匹配 -->
<input type="text" readonly> <!-- 会被 :read-only 匹配 -->
<textarea></textarea> <!-- 会被 :read-write 匹配 -->
<textarea readonly></textarea> <!-- 会被 :read-only 匹配 -->
<p contenteditable>hello</p> <!-- 会被 :read-write 匹配 -->
<p>hello</p> <!-- 会被 :read-only 匹配 -->
:placeholder-shown
该伪类比较直白,它会在显示占位符(placeholder)时匹配该元素。
:default
该伪类会匹配一组元素中的默认元素,它可以是以下几种情况之一:
- 对于
<form>
来说,submit
按钮为其默认的<input>
- 对于
type
属性为radio
或checkbox
的<input>
元素来说,拥有checked
属性的元素为默认元素 - 对于
<option>
元素来说,拥有selected
属性的元素为默认元素
<form>
<input type="text" />
<input type="password" />
<input type="number" />
<input type="submit" /> <!-- 会被 :default 选中 -->
</form>
<input type="checkbox" checked /> <!-- 会被 :default 选中 -->
<input type="checkbox" />
<input type="radio" checked /> <!-- 会被 :default 选中 -->
<input type="radio" />
<select>
<option selected>1</option> <!-- 会被 :default 选中 -->
<option>2</option>
</select>
:checked
该伪类会匹配当前选中的选项元素,可以是:
- 当前选中的
radio input
- 当前选中的
checkbox input
- 当前选中的
select option
:indeterminate
该伪类会匹配状态还不确定的输入元素,包含了:
- 一组
type='radio'
的<input>
下的按钮都未被选中时 - 不包含
value
属性的<progress>
元素 - IDL 属性
indeterminate
的值被设置为true
的type='radio'
的<input>
:valid 和 :invalid
:valid
伪类会匹配通过验证的 <input>
元素或其他表单元素;
:invalid
伪类则会匹配未通过验证的 <input>
元素或其他表单元素,比如在 type="url"
的情况下未正确输入 URL;或者在 type="number"
的情况下输入了字母等。
该伪类可以实时高亮用户输入错误的部分:
<input type="email" />
<style>
:valid { background-color: lightgreen; }
:invalid { background-color: lightcoral; }
</style>
比如上述的代码就可以用来实时反馈输入的 Email 格式是否正确,只有在格式正确的时候才会将背景颜色改成浅绿,在格式不正确时为浅红色。
:in-range 和 :out-of-range
:in-range
伪类会匹配值处于 min
和 max
之间的 <input>
元素;
:out-of-range
伪类会匹配值处于 min
和 max
之外的 <input>
元素。
:in-range
和 :out-of-range
只会匹配那些可以接收 min
和 max
作为属性的 input
元素,比如在 type="number"
的情况下。
:in-range
和 :out-of-range
其实是 :valid
和 :invalid
的子集,被 :in-range
匹配的元素同样会被 :valid
匹配,反之亦然。
:required 和 :optional
:required
伪类会匹配拥有 required
属性的 <input>
,<select>
和 <textarea>
元素;
:optional
伪类会匹配没有 required
属性的 <input>
,<select>
和 <textarea>
元素。
伪元素(Pseudo-elements)
看完了伪类,那么伪元素又是什么呢?我们再来看一下 Selector Level 4 的定义:
Similar to how certain pseudo-classes represent additional state information not directly present in the document tree, a pseudo-element represents an element not directly present in the document tree. They are used to create abstractions about the document tree beyond those provided by the document tree.
概括一下就是,伪元素会通过创建一个抽象的元素的方式,来选中不直接存在于文档树中的元素。比如选中第一行的 ::first-line
等。
::before 和 ::after
::before
会在匹配元素的第一个子元素的位置创建一个伪元素;
::after
会在匹配元素的最后一个子元素的位置创建一个伪元素。
我们可以通过改变其 content
属性来改变其内容:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<style>
li::after { content: '!'; }
</style>
结果如下:
该元素默认为行内元素,我们可以通过修改其 display
属性来进行改动:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<style>
li::before {
display: block;
content: '';
width: 1rem;
height: 1rem;
background-color: red;
}
</style>
结果如下:
::first-letter 和 ::first-line
::first-letter
会匹配块级元素的第一个字母;
::first-line
会匹配块级元素的第一行。
因为只能匹配块级元素,所以像 <span>
之类的行内元素是无法使用该伪元素进行匹配的。
::marker
::marker
会匹配 <li>
的标记,比如 <ol>
的数字或者 <ul>
的小圆点:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<style>
::marker { color: red; }
</style>
结果如下:
::placeholder
::placeholder
也是十分的直白,它可以匹配各种表单元素的占位符(placeholder):
<input type="text" placeholder="hello">
<style>
::placeholder { color: red; }
</style>
结果如下:
::selection
::selection
会匹配被用户鼠标选中(默认高亮蓝色)的部分:
<p>default</p>
<p class='sel'>styled</p>
<style>
.sel::selection { background-color: cyan; }
</style>
选中后结果如下:
::file-selector-button
::file-selector-button
会选中 type=file
的 <input>
元素的按钮部分:
<input type="file">
<style>
::file-selector-button { color: red; }
</style>
结果如下:
伪类和伪元素的区别
- 伪类以单冒号
:
开头;而伪元素以双冒号::
开头 - 伪类会选中特定状态下的元素;而伪元素需要创建额外的抽象元素
- 一个选择器可以出现多个伪类,但伪元素只能出现一次
辨别伪类和伪元素最好的方法就是去看一下我们想要选择的元素是否直接以一个元素的形式存在于文档树中即可。如果存在,就可以使用伪类;如果不存在,就需要使用伪元素了。
选择器优先级(Selector’s Specificity)
假如我们在不同的选择器下都选择到了某一个元素,一个选择器想让它变成红色,而另一个想让它变成蓝色。那么在这种情况下会发生什么呢?总不能变成紫色吧??
这时候就要引入选择器权重和优先级的概念了。权重可以用于指定选择器的优先级;而优先级决定将哪一个选择器应用到该元素上。
在优先级不同时,优先级更高的样式会覆盖优先级低的样式;在优先级相同时,CSS 中最后的那个样式会被应用到元素上。
权重是如何计算的?
选择器的权重是通过以下方法计算的:
每一个选择器的权重有三个值,分别是 A,B 和 C,它们是通过以下方法来计算的:
- A 为其中 ID 选择器的数量
- B 为其中类选择器,属性选择器,伪类的数量和
- C 为其中类型选择器和伪元素的数量和
- 通用选择器和组合器不参与计算
我们来看几个例子:
* // a=0 b=0 c=0
li // a=0 b=0 c=1
ul li // a=0 b=0 c=2
ul ol+li // a=0 b=0 c=3
h1[value=1] // a=0 b=1 c=1
ul ol li.red // a=0 b=1 c=3
li.red.level // a=0 b=2 c=1
#red // a=1 b=0 c=0
在比较两个选择器的权重时,会先比较 A 的值,若一方更大,则其优先级更高;若 A 的值相同,则比较 B 的值;若 B 的值也相同,才会去比较 C 的值。若 ABC 的值都相同,则两个选择器拥有相同的优先级。
权重计算时的其他条件
除了上面的计算方法之外,权重还会被以下特殊条件所影响:
-
对于用逗号分开的一列选择器, 其中的每一个选择器的权重会单独进行计算
-
:is()
和:not()
伪类的权重等同于其参数中具有最高的权重的选择器的权重 -
类似的,
:nth-child()
和:nth-last-child()
伪类的权重也等同于其参数中具有最高的权重的选择器的权重 -
:where()
伪类的权重为 0
我们再来看一些例子:
h1, h2.red // 权重分别为 a=0 b=0 c=1 以及 a=0 b=1 c=1
.red:is(h1) // a=0 b=1 c=1
.red:is(h1, .bold) // a=0 b=1 c=1
.red:is(h1, #bold) // a=1 b=1 c=0
.red:where(h1) // a=0 b=1 c=0
.red:where(h1, #bold) // a=0 b=1 c=0
无视权重的情况
在以下两种情况下,样式会无视其他选择器的权重而直接进行覆盖。
内联样式
内联样式会覆盖其他样式:
<div id='box' style="background-color: yellow;"></div> <!-- 内联样式 -->
<style>
div {
width: 300px;
height: 300px;
}
#box {
background-color: blue; /* 高权重样式 */
}
</style>
所以结果是黄色的背景。
!important 标识符
- 带有
!important
标识符的样式会覆写其他任何样式(包括内联样式):
<div id='box' style="background-color: yellow;"></div> <!-- 内联样式 -->
<style>
div {
background-color: red !important; /* 低权重样式,但带有 !important flag */
width: 300px;
height: 300px;
}
#box {
background-color: blue; /* 高权重样式 */
}
</style>
所以结果是红色的背景。
小结
本文中提到的各种伪类和伪元素基本都得到了现代浏览器的支持,所以大家可以放心使用。除此之外,其实还有许多支持度不太完善的伪类和伪元素,以及关于 shadow dom 的伪类和伪元素等,都没有在本文展开。有兴趣的朋友可以去下面的链接里面自己看一下噢。
希望本文能让你对 CSS 选择器有更多一点的了解,如果文中有任何错误或不准确的地方,欢迎在评论区指正。