深入了解 CSS :has() – wiki基地


深入了解 CSS :has() – 跨越界限的革命性伪类

在 CSS 的世界里,我们习惯了基于父元素选择子元素,或者基于兄弟元素选择相邻元素。例如,我们可以用 div p 选择 div 内的所有段落,用 h2 + p 选择紧跟在 h2 后面的段落。然而,一个长期存在的痛点是:我们很难(或者说几乎不可能)纯粹用 CSS 根据一个元素包含的后代或跟随的兄弟来选择这个元素本身(或者它更上层的祖先)。

举个例子,如果我们想选中所有包含 <img> 标签的 div 元素,然后给这些 div 添加一个边框,传统的 CSS 选择器无能为力。我们只能通过 JavaScript 来遍历所有 div,检查它们是否包含 <img>,然后为符合条件的 div 添加一个类名,再用 CSS 样式化这个类。这打破了关注点分离的原则,增加了复杂度。

这种情况随着 :has() 伪类的出现而彻底改变了。:has() 被誉为 CSS 选择器的一次革命,因为它引入了“父选择器”的概念(尽管它的能力远不止于选择父元素)。它允许我们根据一个元素内部或其后的内容来选择这个元素自身或其祖先元素,从而以前所未有的方式构建样式规则,极大地增强了 CSS 的表现力。

什么是 CSS :has()?

:has() 伪类接收一个或多个选择器作为参数。它的基本语法是 :has(selectorList)。当应用到一个元素上时,:has() 会检查该元素的后代元素或紧随其后的兄弟元素是否能匹配 selectorList 中的任何一个选择器。如果匹配成功,那么应用 :has() 的这个元素就会被选中。

简单来说:

A:has(B):选择所有包含符合选择器 B 的元素的 A 元素。这里的 B 可以是 A 的任意后代,也可以是紧随 A 的兄弟元素。

例如:

  • article:has(h1):选择所有包含一个 <h1> 元素的 <article> 元素。
  • div:has(.error):选择所有包含一个类名为 error 的元素的 <div> 元素。
  • ul:has(li:last-child):选择所有包含一个作为最后一个子元素的 <li><ul> 元素。
  • input:has(+ label):选择所有紧跟着一个 <label> 元素的 <input> 元素。

注意,:has() 检查的是是否存在匹配的元素,而不是匹配元素的数量。

为什么说它是“革命性”的?理解其核心价值

:has() 之所以被认为具有革命性,是因为它打破了 CSS 选择器长期以来的一个单向限制:从左到右、从上到下(在文档树中)。传统的选择器只能基于元素的自身类型、类、ID、属性,以及它与其前辈(父、祖先、前面的兄弟)的关系来选择它。我们无法根据它与后辈(子、后代)或后面的兄弟的关系来选择它自身或其前辈。

举例说明这个限制:

  • div img:选中 div 里的 img。可以。
  • div > img:选中 div 的直接子 img。可以。
  • img + p:选中紧跟在 img 后面的 p。可以。
  • img ~ p:选中跟在 img 后面的所有 p。可以。

但你不能用 img < div (没有这样的语法) 来选中包含 imgdiv,或者用 p + img (错误语义) 来选中紧跟在 p 前面的 img

:has() 改变了这一切。它允许我们将“目光”投向元素的内部或其后的内容,然后根据这些内容的存在或特征来决定是否选中外部的、靠前的那个元素。这就像赋予了 CSS “反向查找”的能力。

例如,以前我们想给一个有图片的卡片 (.card) 添加一个特殊的背景色,没有 :has() 的话,通常需要在包含图片的 .card 上额外添加一个类 (.card.has-image),或者用 JavaScript 来判断并添加类。有了 :has(),一行 CSS 就可以搞定:

css
.card:has(img) {
background-color: #f0f0f0; /* 给所有包含 img 的 .card 添加背景色 */
}

这极大地简化了代码,提高了 CSS 的表达能力,并有助于将样式逻辑保留在 CSS 文件中,而不是散布在 HTML 类名或 JavaScript 代码里。

虽然常被称为“父选择器”,但 :has() 的能力不止于此。它实际上是基于相对选择器来判断的,这个相对选择器可以匹配后代或兄弟。因此,A:has(B) 可以选择 A 如果它:

  1. 包含一个 B 后代 (A:has(B))
  2. 包含一个 B 直接子元素 (A:has(> B))
  3. 紧跟着一个 B 兄弟元素(这里 A 是 B 的前一个兄弟,选择器写为 A:has(+ B),选择的是 A)
  4. 后跟着一个 B 兄弟元素 (A:has(~ B))

这个概念非常重要。:has() 的主体是应用 :has() 的那个元素(即 : 前面的那个选择器匹配的元素),它会检查圆括号内的相对选择器是否能从该元素的内部或紧随其后开始匹配到内容。

实用场景举例

:has() 的应用场景非常广泛,几乎涵盖了所有需要根据子元素状态或存在性来调整父元素或祖先元素样式的场景。以下是一些常见的实用案例:

1. 根据子元素的存在性或类型样式化父元素/容器:

  • 有图片的卡片:

    “`html

    标题

    一些描述。

    另一个标题

    图片

    一些描述。

    “`

    “`css
    .card:has(img) {
    border: 2px solid royalblue;
    padding: 15px;
    }

    .card:not(:has(img)) {
    background-color: #eee;
    }
    “`

    这段 CSS 会给所有包含 <img>.card 添加蓝色边框和内边距,给所有不包含 <img>.card 添加浅灰色背景。

  • 空状态提示:

    “`html


    没有数据。

    “`

    如果列表为空,我们可能想显示一个“没有数据”的消息,并隐藏列表。传统做法是 JavaScript 控制显示/隐藏。有了 :has(),如果列表不包含任何 <li>,就隐藏列表,显示消息:

    html
    <div class="list-container">
    <ul class="item-list">
    <!-- <li>列表项...</li> -->
    <!-- 如果没有 li 标签,ul 就是空的 -->
    </ul>
    <div class="empty-message">没有数据。</div>
    </div>

    “`css
    / 默认隐藏空消息 /
    .list-container .empty-message {
    display: none;
    }

    / 如果 list-container 不包含 li,显示空消息 /
    .list-container:not(:has(.item-list li)) .empty-message {
    display: block;
    }

    / 如果 list-container 不包含 li,隐藏列表 /
    .list-container:not(:has(.item-list li)) .item-list {
    display: none;
    }
    “`

2. 根据子元素的状态样式化父元素/容器(例如表单验证):

  • 输入框无效时高亮其父容器:

    “`html



    “`

    “`css
    / 当 form-group 包含一个无效的 input 元素时,给 form-group 添加边框 /
    .form-group:has(input:invalid) {
    border: 1px solid red;
    padding: 10px;
    }

    / 可选:当 input 有效时移除边框 /
    .form-group:has(input:valid) {
    border: none; / 或其他有效状态样式 /
    }
    “`

    这使得表单验证的视觉反馈可以直接通过 CSS 实现,无需 JavaScript 监听输入事件并操作类名。

  • 复选框选中时样式化相关联的元素:

    html
    <div class="toggle-feature">
    <input type="checkbox" id="enable-advanced">
    <label for="enable-advanced">启用高级设置</label>
    <div class="advanced-options">
    <!-- 一些高级设置的内容 -->
    <p>这里是一些只有启用高级设置后才需要看的内容。</p>
    </div>
    </div>

    “`css
    / 默认隐藏高级设置 /
    .advanced-options {
    display: none;
    }

    / 当 toggle-feature 容器中,id 为 enable-advanced 的 checkbox 被选中时,显示高级设置 /
    .toggle-feature:has(#enable-advanced:checked) .advanced-options {
    display: block;
    margin-top: 10px;
    padding: 10px;
    border: 1px dashed #ccc;
    }

    / 可选:当 checkbox 被选中时,样式化 label 或容器本身 /
    .toggle-feature:has(#enable-advanced:checked) label {
    font-weight: bold;
    color: green;
    }

    .toggle-feature:has(#enable-advanced:checked) {
    background-color: #e9f5e9; / 轻微的背景色提示 /
    }
    “`

    这是一个非常强大的模式,常用于构建复杂的交互式组件,比如折叠面板、带有可选项的表单等,极大地减少了对 JavaScript 的依赖。

3. 根据兄弟元素(后一个)的存在性或类型样式化当前元素或其祖先:

  • 紧跟图片后面的段落特殊样式: (前面提到这个需要注意语法)

    如果你想样式化紧跟在图片后面的段落,传统的 img + p 就可以。但如果你想样式化前面是图片的段落的容器,或者前面紧跟着一个段落的图片:has() 就派上用场了。

    • 样式化紧跟着图片后面的段落的父容器:

      html
      <div class="content-block">
      <p>这是一段文本。</p>
      <img src="image.jpg" alt="图片">
      <p class="caption">这是图片的说明文字。</p>
      </div>

      css
      /* 选择包含一个图片紧跟着一个带有 class="caption" 的段落的 .content-block */
      .content-block:has(img + p.caption) {
      border: 2px dashed orange;
      padding: 10px;
      }

    • 样式化紧跟着图片后面的段落 (使用 :has() 找到父元素,再选择子元素): 尽管直接使用 img + p 更简洁,但这展示了 :has() 如何与后代选择器结合。

      css
      /* 选择包含 img 且 img 后面跟着 p 的 div,然后选择 div 中的 p */
      div:has(img + p) p {
      color: blue; /* 这样会选中 div 中所有 p,除非 p 在 img 后面 */
      }
      /* 更精确的:选择包含 img + p 结构的 div,然后选择这个结构中的 p */
      div:has(img + p) img + p {
      color: blue; /* 这等同于 img + p,但展示了思维过程 */
      }

      这里的关键在于,:has() 让你能基于内部/后续结构先选中外部/靠前元素,然后再基于这个外部/靠前元素向下或向后选择。

  • 没有后续文本的标题特殊样式: (比如章节的最后一个标题)

    “`html

    章节一

    内容…

    章节二


    “`

    css
    /* 选择所有包含 h2 且 h2 后面没有紧跟着 p 的 section */
    /* 注意:h2:not(:has(+ p)) 会选择 h2 本身,如果 h2 后面没有紧跟着 p */
    /* 如果要选择 section,需要这样: */
    section:has(h2:last-child):not(:has(h2 + *)) h2 {
    color: gray; /* 如果 section 最后一个子元素是 h2,并且 h2 后面没有任何兄弟元素,则样式化这个 h2 */
    }
    /* 或者更简单的判断方式,如果 h2 后面没有任何兄弟元素 */
    h2:last-child:not(:has(+ *)) {
    color: gray; /* 样式化 h2 本身 */
    }

    这个例子有点复杂,展示了如何结合 :not():has() 来表达“不存在某个结构”的条件。

4. 复杂布局调整:

  • 网格或 Flex 容器中,子元素数量不足时的布局调整:

    假设一个网格容器应该显示 3 列,但只有 1 或 2 个子元素。我们可能希望这些子元素居中而不是靠左对齐。

    html
    <div class="grid-container">
    <div class="grid-item">1</div>
    <div class="grid-item">2</div>
    <!-- <div class="grid-item">3</div> -->
    </div>

    “`css
    .grid-container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
    }

    / 如果容器不包含第三个 grid-item,则让内容居中 /
    .grid-container:not(:has(.grid-item:nth-child(3))) {
    place-items: center; / 或者 justify-content: center; 如果是 flexbox /
    grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); / 也可以调整列模板 /
    }
    “`

    这个例子展示了如何根据容器内的元素数量来调整容器自身的布局属性。

5. 辅助可访问性 (Accessibility):

  • 当容器内部的元素获得焦点时,高亮整个容器: 使用 :focus-within 伪类也可以实现类似效果,但 :has(:focus) 提供了另一种更灵活的方式,可以结合其他条件。

    html
    <div class="interactive-card" tabindex="0"> <!-- 容器可聚焦,或者其子元素可聚焦 -->
    <h3>标题</h3>
    <p><a href="#">链接1</a></p>
    <p><button>按钮</button></p>
    <p><a href="#">链接2</a></p>
    </div>

    “`css
    / 当 .interactive-card 内部的任何可聚焦元素获得焦点时,高亮卡片 /
    .interactive-card:has(:focus) {
    outline: 2px solid blue;
    outline-offset: 2px;
    }

    / 更精确地针对特定可聚焦元素 /
    .interactive-card:has(a:focus, button:focus) {
    box-shadow: 0 0 10px rgba(0, 0, 255, 0.5);
    }
    “`

    这对于用户使用键盘导航时非常有用,能清晰地指示当前焦点所在的区域。

语法和高级用法

:has() 的强大之处在于其参数可以是一个复杂或包含多个选择器的列表。

  • 包含多个选择器: :has(selector1, selector2) 选中包含 selector1selector2 的元素。

    css
    /* 选择包含 h1 或 h2 的 article */
    article:has(h1, h2) {
    border-top: 5px solid red;
    }

  • 与组合器结合:

    “`css
    / 选择直接子元素中包含 .highlight 的 div /
    div:has(> .highlight) { / 注意 > 符号 /
    background-color: yellow;
    }

    / 选择紧跟在 img 后面的 p 的 div /
    div:has(img + p) {
    padding-bottom: 20px;
    }

    / 选择包含一个 h2 且 h2 后面有 p 的 section /
    section:has(h2 ~ p) {
    margin-bottom: 30px;
    }
    “`

  • 与伪类/伪元素结合: 可以在 :has() 参数中使用几乎所有伪类和伪元素(当然,伪元素通常是元素的最后一个部分,所以 :has() 检查到它们本身意义不大,更多是检查包含它们的元素)。

    “`css
    / 选择包含一个被 hover 的链接的导航菜单 /
    nav:has(a:hover) {
    background-color: #f8f8f8;
    }

    / 选择包含一个在第三个位置的列表项的无序列表 /
    ul:has(li:nth-child(3)) {
    list-style-type: square;
    }

    / 选择包含一个必需输入框但尚未填写的表单 /
    form:has(:required:placeholder-shown) {
    border: 2px dashed orange;
    }
    “`

  • 链式使用 :has() 可以连续使用 :has() 来表达更复杂的条件。

    “`css
    / 选择一个 div,它包含一个 class 为 item 的元素,并且这个 item 元素内部又包含一个 img 标签 /
    div:has(.item:has(img)) {
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
    }

    / 选择一个容器,它包含一个处于 active 状态的子元素,并且这个子元素内部有一个带有 title 属性的 span /
    .container:has(.active:has(span[title])) {
    border: 1px solid green;
    }
    “`

  • :not() 结合使用 :has() 这是表达“不包含”或“不存在”条件的强大方式。

    “`css
    / 选择所有不包含 img 的 div /
    div:not(:has(img)) {
    border: 1px dashed #ccc;
    }

    / 选择所有不包含 h1 且不包含 h2 的 article /
    article:not(:has(h1, h2)) {
    font-style: italic;
    }
    “`

    css
    /* 选择一个列表项,如果它不是最后一个列表项并且后面没有紧跟着一个带有 class 为 'separator' 的列表项 */
    /* 这个例子比较绕,慎用复杂组合 */
    li:not(:last-child):has(+ li:not(.separator)) {
    margin-right: 10px; /* 给除了最后一个、且后面跟着的不是分隔符 li 的 li 添加右边距 */
    }

性能考虑

:has() 需要浏览器在匹配元素时向其内部(或紧随其后)进行查找。这相比于传统的从左到右的查找似乎会增加一些开销。然而,现代浏览器引擎在选择器匹配方面进行了大量优化。

规范和浏览器开发者认为,对于大多数合理的 :has() 使用场景,性能影响是微乎其微的,甚至可能比使用 JavaScript 实现相同效果更高效(JS 需要遍历 DOM,操作类名,这可能导致回流和重绘)。浏览器可以对 :has() 选择器进行优化,例如在发现不匹配时及时停止查找。

过于复杂、深度嵌套或涉及大量兄弟查找的 :has() 选择器理论上可能比简单的选择器开销更大。但对于常见的“父根据子状态变化”的场景,性能通常不是问题。过度优化不可取,只有在实际测量到性能瓶颈时,才需要考虑简化 :has() 或寻找替代方案。

浏览器支持

在很长一段时间里,:has() 只是一个概念或实验性功能。但现在,:has() 已经在主流浏览器中获得了广泛支持:

  • Chrome 和 Edge (从版本 105 开始)
  • Firefox (从版本 105 开始)
  • Safari (从版本 15.4 开始)

这意味着 :has() 已经可以安全地在生产环境中使用,无需担心兼容性问题(除非需要支持非常旧的浏览器版本)。这是 :has() 真正成为 CSS 强大工具的关键一步。

与 JavaScript 的对比

:has() 出现之前,许多“根据后代状态改变祖先样式”的需求只能通过 JavaScript 实现。现在,:has() 提供了一种声明式的、纯 CSS 的解决方案。

何时使用 :has()

  • 样式是基于元素的静态结构或动态伪类状态(如 :checked, :invalid, :hover, :focus 等)的。
  • 你只需要改变样式,不需要改变元素的行为、内容或结构。
  • 希望将样式逻辑保留在 CSS 文件中,提高代码的可维护性和关注点分离。
  • :has() 能简洁高效地表达所需选择器。

何时使用 JavaScript:

  • 需要根据用户交互(点击、拖拽等)以外的事件来改变样式。
  • 需要改变元素的结构(添加、删除元素)。
  • 需要改变元素的文本内容或属性(非样式属性)。
  • 需要获取或处理复杂的运行时数据来决定样式。
  • :has() 表达所需逻辑过于复杂或不可能实现(例如,根据元素的计算样式或屏幕外部状态来改变样式)。

总的来说,对于基于 DOM 结构和标准伪类状态的样式需求,:has() 往往是更优的选择,因为它更符合 CSS 的声明式特性,且通常在性能和代码清晰度上更有优势。

使用中的注意事项和潜在问题

  • 可读性: 虽然强大,但过于复杂或嵌套的 :has() 选择器会降低 CSS 代码的可读性。尽量保持选择器简洁明了。如果一个 :has() 选择器变得难以理解,考虑拆分规则或重新思考 HTML 结构。
  • 特异性 (Specificity): :has() 伪类本身不增加特异性点数。然而,它内部的参数贡献特异性。例如,div:has(.item) 的特异性是 div 的特异性加上 .item 的特异性。这可能导致一些意想不到的特异性冲突,需要仔细管理。
  • 无限循环 (Infinite Loops) 保护: CSS 规范包含规则来防止 :has() 选择器陷入无限循环。例如,:has() 不能包含会反向匹配到自身祖先的相对选择器,也不能匹配到伪元素本身(因为伪元素不能包含其他元素)。浏览器会检测并拒绝或忽略这些无效的 :has() 用法。例如,div:has(div) 是有效的,但 div:has(:scope div) (试图匹配其内部的 div,然后用相对选择器匹配到自身) 可能受限,具体行为取决于规范细节和浏览器实现。对于日常使用,遇到这些高级循环问题的可能性不大。
  • 性能: 再次强调,对于 大多数 用例,性能不是问题。但在大型、复杂的应用中,如果遇到性能瓶颈,:has() 选择器是需要考虑检查和优化的点之一。

结论

CSS :has() 伪类是现代 CSS 中最令人兴奋的新增功能之一。它弥补了 CSS 选择器长期以来的一个核心缺陷,赋予了开发者基于后代或后续兄弟状态来样式化祖先或靠前元素的能力。这不仅仅是一个新的选择器,它改变了我们思考和编写 CSS 的方式,使得许多以前需要 JavaScript 或冗余类名才能实现的效果,现在可以用纯 CSS 以更声明式、更简洁的方式实现。

:has() 极大地提升了 CSS 的表现力,让我们可以构建更灵活、更健壮、更易于维护的用户界面组件。随着浏览器支持的普及,:has() 已经成为现代 Web 开发中一个不可或缺的工具。掌握并善用 :has(),将能够写出更强大、更优雅的 CSS 代码。现在就开始在你的项目中使用它,体验它带来的巨大便利和可能性吧!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部