CSS 有多种选择器,可以帮助我们针对不同的 HTML 标签设置样式。

选择器


通配符选择器

通配符选择器(*)可以选择任何元素,通常用于设置默认样式。

* {
  margin: 0;
  padding: 0;
}

类型选择器

类型选择器(Type Selectors)直接使用 HTML 标签来选择选择元素:

p { color: blue; }

上面的代码将所有的 <p> 标签文本渲染为蓝色。

class 选择器

个人理解 class 选择器代表着 “一类有着相同样式的标签”,所以该选择器可以用于多个 HTML 标签。

类选择器使用 . 开头,后接类名:

<style>
    .first {
        font-weight: bold;
        text-decoration: line-through;
    }
</style>

<p class="first">超越技术</p>
<div>哈哈.</div>
<p>一起学前端!</p>

一个标签也可以包含多个 class,多个 class 之间使用空格分隔:

<!-- 包含两个 class:info 和 highlight -->
<p class="info highlight">...</p>

此外,当你在不同的 HTML 标签中使用了相同的 class,然后还想选择某个具体的标签时,你可以像下面这样组合使用两个选择器:

<style>
    p.first { color: red; }
</style>

<p class="first">该颜色会是红色</p>
<span class="first">该颜色不会是红色</span>

id 选择器

个人理解这里的 id 在使用场景上就和数据库中的主键一样,可以用来定位某个唯一的 HTML 标签,所以该选择器或者说 id 属性的值,在 HTML 文档中需要是唯一的,不能出现在多个元素上。

id 选择器使用 # 号来定义:

<style>
    #my-id {
    background-color: yellow;
    }
</style>

<p id="my-id">超越技术</p>
<div>哈哈.</div>
<p>一起学前端!</p>

因为 id 选择器的唯一性,所以就没必要像上面提到的 class 选择器一样,通过标签再进一步缩小选择范围了。

属性选择器

我们都知道可以在 html 标签上自定义任意的属性,比如下面的例子:

<!-- 
    `data-rakuyo` 不是很好,因为它不明所以;
    但是它又很好,可以一眼让你看出它是一个自定义属性。 
-->
<div data-rakuyo>...</div>

属性选择器使用 [] 中括号包裹属性名称。针对上面的代码,我们可以通过属性选择器来选择该 div 标签:

/* 选择所有带有 `data-rakuyo` 属性的元素,不论其所属哪个 html 标签 */
[data-rakuyo] { font-weight: bold; }

再进一步缩小范围,属性选择器可以通过指定标签来筛选 “具有某些属性的标签”:

/* 仅限带有 `data-style` 属性的 <button> 标签 */
button[data-style] { font-weight: bold; }

可以通过具体的属性值来做条件筛选:

<style>
  [data-style="cancel"] { color: gray; }
  [data-style="done"] { color: black; }
</style>

<button data-style="cancel">取消</button>
<button data-style="done">下单</button>

属性选择器还可以借助一些匹配操作符,比如 $=*= 来实现特定的功能:

/* 选择 `src` 属性以 `https` 开头的所有图片 */
img[src^="https"] { color: red; }

/* 选择 `src` 属性以 `.jpg` 结尾的所有图片 */
img[src$=".jpg"] { border: 3px solid black; }

/* 选择 `href` 属性包含 `external` *字符串* 的所有链接 */
a[href*="external"] { color: green; }

/* 选择 `href` 属性包含 `external` *单词* 的所有链接 */
a[href~="external"] { color: green; }

注意 *=~= 的区别。前者匹配字符串,而后者匹配的是一个完整的单词。

伪选择器

伪选择器包含 “伪类选择器” 和 “伪元素选择器” 两种。

伪类选择器

伪类选择器用于定义元素在特定状态下的样式,比如鼠标悬停时的样式。或者用于选择第n个子元素。

对于特定状态,有以下几种:

名称 说明 示例
:hover 用于在用户将鼠标悬停在元素上时应用样式

a:hover { 
    color: red; 
}
            
:active 用于选取被用户激活(例如鼠标点击)的元素

button:active {
    background-color: gray;
}
        
:visited 用于选取用户已访问过的链接

a:visited {
    color: purple;
}
        
:focus 用于选取当前获取了焦点的元素
通常在用户通过键盘或者鼠标进行交互时出现

input:focus {
    border-color: blue;
}         
        

对于选择子元素,有以下几种用法:

名称 说明 示例
:nth-of-type(n) 该选择器允许你选择相同类型的且特定位置的子元素

请注意,n从1开始,且不能是负数

/* 这会使每隔两个段落文字变为红色 */
p:nth-of-type(2n) {
    color: red;
}
            
:nth-child(n) 该选择器允许你选择特定位置的子元素

/* 列表中的奇数项背景色变为浅灰色 */
li:nth-child(odd) {
    background-color: lightgray;
}
            
:nth-last-child(n) 该选择器与:nth-child(n)类似,但是它从元素的末尾开始计数。

/* 
 * 这会使每个无序列表(ul)的
 * 倒数第二个列表项(li)的文字颜色变为蓝色 
 */
ul li:nth-last-child(2) {
    color: blue;
}
            
:first-child

“nth-child(1)”
该选择器选取某个元素的第一个子元素

/* 
 * 这会使每个无序列表(ul)的第一个列表项(li)的文字加粗 
 */
ul li:first-child {
    font-weight: bold;
}
            
:last-child

“nth-last-child(1)”
该选择器选取某个元素的最后一个子元素

/* 
 * 这会使每个无序列表(ul)
 * 的最后一个列表项(li)的文字变为斜体 
 */
ul li:last-child {
    font-style: italic;
}
            

我觉得 :nth-of-type:nth-child 的概念比较接近,同时容易混淆,所以在这里特意将他们两个单独拿出来,用一个例子来看看这两个选择器之间的区别。

<div class="container">
    <p>Paragraph 1</p>
    <div>Div 1</div>
    <p>Paragraph 2</p>
    <p>Paragraph 3</p>
    <div>Div 2</div>
</div>

针对上面这段 html,两个选择器的结果为:

选择器 结果 说明
.container p:nth-of-type(2) 选择到 <p>Paragraph 2</p> 这个选择器将选择容器 .container 内的第二个 <p> 元素。在给定的 HTML 结构中,第二个 <p> 元素是 "Paragraph 2",因此该选择器会影响到这个段落元素。
.container p:nth-child(2) 不会选择任何元素 这个选择器将选择容器 .container 内所有的 <p> 元素中的第二个子元素。在给定的 HTML 结构中,第二个子元素是 "Div 1",而不是 <p> 元素。因此,这个选择器不会选择任何元素,因为在该结构中,第二个子元素不是 <p> 元素。

简而言之,nth-child(n) 会先按照顺序选择父视图的子元素,再看该子元素是否符合限制条件,如果符合则样式生效,否则不会生效;而 nth-of-type(n) 则相反,会先选择符合要求的元素,再按照顺序进行选择。

同理可得,因为 :first-child:last-child:nth-child(1):nth-last-child(1) 相同,所以假如使用 p:fist-child,但是第一个标签不是 <p>,那么该样式也不会生效。

伪元素选择器

伪元素选择器用于选择元素的特定部分而不是元素本身。伪元素选择器以双冒号 :: 开头。

下面是一些常见的伪元素选择器:

/* 选择元素的第一行文本 */
p::first-line {
    font-weight: bold;
}

/* 选择元素的第一个字母 */
p::first-letter {
    font-size: 150%;
}

/* 在元素内容之前插入内容 */
p::before {
    content: "前置内容 ";
}

/* 在元素内容之后插入内容 */
p::after {
    content: " 后置内容";
}

/* 选择用户选中的文本部分 */
::selection {
    background-color: yellow;
    color: black;
}

/* 选择输入框的占位符文本 */
input::placeholder {
    color: gray;
}

除此之外还有一些不太常见的伪元素选择器:

/* 选择列表项的标记部分(通常是列表项前面的符号,如圆点或数字) */
li::marker {
    color: red;
}

/* 选择元素的背景层,用于处理全屏元素的背景样式 */
dialog::backdrop {
    background-color: rgba(0, 0, 0, 0.5);
}

/* 选择拼写错误的文本部分 */
::spelling-error {
    text-decoration: underline wavy red;
}

/* 选择语法错误的文本部分 */
::grammar-error {
    text-decoration: underline dashed blue;
}

组合选择器

组合选择器是 CSS 中的一种选择器,允许你针对同时满足多个条件的元素应用样式。

后代选择器

后代选择器(Descendant Selector)允许你选择某个元素内部的所有后代元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Descendant Selector Example</title>
  <style>
    /* 选择 .container 内部的所有 p 元素 */
    .container p {
      color: blue;
    }
  </style>
</head>
<body>
  <div class="container">
    <p>This is a paragraph.</p>
    <div>
      <p>This is another paragraph.</p>
    </div>
  </div>
</body>
</html>

在这个例子中,.container p 选择器会选择所有嵌套在 .container 内部的 <p> 元素,并将它们的颜色设为蓝色。即两个 <p> 标签都会变成蓝色。

子选择器

子选择器(Child Selector)用于选择某个元素的直接子元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Child Selector Example</title>
  <style>
    /* 选择 .container 下的直接子元素 p */
    .container > p {
      font-weight: bold;
    }
  </style>
</head>
<body>
  <div class="container">
    <p>This is a paragraph.</p>
    <div>
      <p>This is another paragraph.</p>
    </div>
  </div>
</body>
</html>

这个例子中,.container > p 选择器只会选择 .container 直接子元素中的 <p> 元素,并将它们的字体加粗。而里层的 This is another paragraph. 并不会被加粗。

对比 后代选择器 来看,后代选择器会一直查询到该叶子节点的最末端,渲染所有满足条件的标签,不管中间是否还有其他标签;而 子选择器 只会渲染下一个层级(直接)的子节点,并不会进一步深入。

相邻兄弟选择器

相邻兄弟选择器(Adjacent Sibling Selector)选择紧接在指定元素后的兄弟元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Adjacent Sibling Selector Example</title>
  <style>
    /* 选择 .container 后面紧邻的 p 兄弟元素 */
    .container + p {
      color: green;
    }
  </style>
</head>
<body>
  <div class="container">
    <p>This is a paragraph.</p>
  </div>
  <p>This is another paragraph.</p>
</body>
</html>

在这个例子中,.container + p 选择器会选择紧接在 .container 元素后面的 <p> 元素,并将它们的颜色设为绿色。即 This is another paragraph. 会被渲染为绿色。

有以下几点需要注意:

  1. “相邻兄弟选择器” 强调 相邻。如果我们将代码进行修改:
<div class="container">
    <p>This is a paragraph.</p>
</div>
<span>123</span>
<p>This is another paragraph.</p>

此时 .container 不再有任何相邻的 <p> 节点,所以 .container + p 不会选择任何的标签进行渲染。

  1. “相邻兄弟选择器” 只会渲染相邻的 一个 节点。如果我们将代码进行修改:
<div class="container">
    <p>This is a paragraph.</p>
</div>
<p>This is another paragraph. 1</p>
<p>This is another paragraph. 2</p>

那么只有相邻的第一个 <p> 标签,即 This is another paragraph. 1 会被渲染为绿色,另外一个 <p> 标签仍为黑色。

通用兄弟选择器

通用兄弟选择器(General Sibling Selector)选择与指定元素相邻的所有兄弟元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>General Sibling Selector Example</title>
  <style>
    /* 选择 .container 后的所有 p 兄弟元素 */
    .container ~ p {
      font-style: italic;
    }
  </style>
</head>
<body>
  <div class="container">
    <p>This is a paragraph.</p>
  </div>
  <p>This is another paragraph.</p>
  <p>This is yet another paragraph.</p>
</body>
</html>

在这个例子中,.container ~ p 选择器会选择紧跟在 .container 元素后的所有 <p> 元素,并将它们的字体样式设为斜体。很容易看出来,该选择器和 相邻兄弟选择器 互为一对。

组合选择器总结

后代选择器( 子选择器(> 互为一对:同样是选择子元素,前者会深入到层级的最末端,而后者只会选择下一个层级,点到为止。

相邻兄弟选择器(+通用兄弟选择器(~ 互为一对:同样是选择相邻元素,前者只选择同层级内相邻的节点,而后者会选择同层级内所有符合条件的节点。

课后思考

现在我们提出来这么一个需求:从接口处获取商品的价格(忽略接口请求步骤),得到一个价格字符串,比如 "¥35.66"。针对该字符串进行渲染,要求如下:

  1. 文字颜色均为红色。
  2. ¥.66 部分样式一致:15号字。但是需要注意 ¥ 符号并不一定存在。
  3. 35 部分加粗,18号字。

使用 js+html+css,留几秒钟思考给出答案。


...
...
...
...
...


参考答案如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        /* 样式1:¥符号 15 号字,红色 */
        .currency-sign,
        .after-dot {
            font-size: 15px;
        }

        /* 样式2:¥ 和 . 之间的内容,18号字,加粗,红色 */
        .between {
            font-size: 18px;
            font-weight: bold;
        }

        .currency-sign,
        .after-dot,
        .between {
            color: red;
        }
    </style>
</head>
<body>
    <span class="currency-value"></span>

    <!-- 使用 JavaScript 定义变量 -->
    <script>
        // 定义变量,代替接口请求的步骤
        const currencyValue = "¥35.66";

        // 获取 span 标签
        const spanElement = document.querySelector('.currency-value');

        // 检查 span 标签是否存在
        if (spanElement) {
            // 检查 currencyValue 是否包含 "¥" 符号
            if (currencyValue.includes('¥')) {
                // 渲染 ¥ 符号
                const currencySignSpan = document.createElement('span');
                currencySignSpan.innerText = '¥';
                currencySignSpan.classList.add('currency-sign');
                spanElement.appendChild(currencySignSpan);
            }

            // 删去 "¥" 符号后的值
            let valueWithoutSign = currencyValue.replace('¥', '');

            // 按 "." 分隔值
            const [beforeDot, afterDot] = valueWithoutSign.split('.');

            // 渲染 "." 之前的内容
            const beforeDotSpan = document.createElement('span');
            beforeDotSpan.innerText = beforeDot;
            beforeDotSpan.classList.add('between');
            spanElement.appendChild(beforeDotSpan);

            // 渲染 "." 符号 和 "." 之后的内容
            const afterDotSpan = document.createElement('span');
            afterDotSpan.innerText = '.' + afterDot;
            afterDotSpan.classList.add('after-dot');
            spanElement.appendChild(afterDotSpan);
        }
    </script>
</body>
</html>

然后我们可以再进一步,如果改成 vue3 + TypeScript 呢?

<template>
  <span class="currency-value">
    <span v-if="hasCurrencySign" class="currency-sign">¥</span>
    <span class="between">{{ beforeDot }}</span>
    <span class="after-dot">.{{ finalAfterDot }}</span>
  </span>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const currencyValue = ref<string>("¥35.66");

const hasCurrencySign = currencyValue.value.includes('¥');
const [beforeDot, afterDot] = currencyValue.value.replace('¥', '').split('.');
const finalAfterDot = afterDot || '00';
</script>

<style scoped>
/* 样式1:¥符号 15 号字,红色 */
.currency-sign,
.after-dot {
    font-size: 15px;
}

/* 样式2:¥ 和 . 之间的内容,18号字,加粗,红色 */
.between {
    font-size: 18px;
    font-weight: bold;
}

.currency-sign,
.after-dot,
.between {
    color: red;
}
</style>

可见因为 vue 中的模版可以直接引用变量,为我们减少了大量的 DOM 操作,代码量得以减少了不少。