全部 / 前端 / 技术 · 2022年4月22日 0

CSS 父选择器

你是否想过 CSS 选择器可以检测父元素下面包含特定的元素存在?例如,如果一个 card 组件包含一个缩略图,我们需要添加display:flex 样式。在之前的 CSS 中是不可能的但现在我们有了新的选择器,:has 选择器可以帮助我们选出包含特定元素的父级以及其他功能。

在这篇文章中,我会解释 :has 解决了什么问题,并通过案例来说明它是如何工作以及在哪可以使用,今天最重要的事我们如何使用它。

问题

之前根据一个元素是否存在来给父级设置样式是不可能的,我们需要根据变动来切换 CSS 的类。

考虑一下接下来的基础例子:

我们有一个 card 组件,它有两种形态:1. 有图片;2.无图片。在 CSS 中,我们需要像下面写代码:

/* A card with an image */
.card {
    display: flex;
    align-items: center;
    gap: 1rem;
}

/* A card without an image */
.card--plain {
    display: block;
    border-top: 3px solid #7c93e9;
}
<!-- Card with an image -->
<div class="card">
    <div class="card__image">
        <img src="awameh.jpg" alt="">
    </div>
    <div class="card__content">
        <!-- Card content here -->
    </div>
</div>

<!-- Card without an image -->
<div class="card card--plain">
    <div class="card__content">
        <!-- Card content here -->
    </div>
</div>

就像上面看到的,由于没有图片的 card 我们不需要 flex 来包裹所以创建了一个特定的 class 来指定。那现在问题是,我们是否在 CSS 中做条件判断,而不需要动态的 class ?

这就需要 :has 来救场了,它可以帮助我们 .card 是否包含 .card_image

例如,我们可以检测 card 元素 :has an image,若包含,我们需要给它加上 flexbox 属性。

.card:has(.card__image) {
    display: flex;
    align-items: center;
}

介绍 CSS :has 选择器

根据 CSS spec :has 选择器是检测父级是否包含指定的元素或 input 是否获取焦点。

让我们再次看看上面例子的代码片段:

.card:has(.card__image) { }

我们检测 .card 父级是否包含 .card_image 子元素。细看下面的图例:

用文字描述,上面 CSS 选择器等同于下面:

card 是否包含 card__image 元素?

是不是很神奇?我们在 CSS 中添加了逻辑,写 CSS 多么没好的时光!

:has 选择器不仅仅和 Parent 有关

它不仅仅能检测父级是否包含一个特定元素,例如,我们还可以用她检测一个元素是否跟着 <p> 元素。看看下面:

.card h2:has(+ p) { }

这就是检测了 <h2> 元素是否跟着 <p> 元素。或者我们可以用在检测 form 元素是否有一个 focused 的 input ,例如:

/* If the form has a focused input, apply the following */
form:has(input:focused) {
    background-color: lightgrey;
}

浏览器支持情况

截止到写作的时间,CSS :has 只被 Safari 15.4 和 Chrome Canary 支持。保持对 Can I use 上的支持情况。

我们可否用它作为渐进增强的方案?

当然可以,我们可以通过 @support 规则来检测是否支持 :has

@supports selector(:has(*)) {
    /* do something */
}

好了,理论只是已经讲了很多,接下来看看实际案例:

CSS :has 的案例

Section Header

当我书写 Section Header 时,经常遇到两种情况,一个只包含标题另一个既包含标题也包含链接。

依据是否存在链接,我希望添加不同的样式。

<section>
    <div class="section-header">
        <h2>Latest articles</h2>
        <a href="/articles/>See all</a>
    </div>
</section>

注意,我使用了 :has(>a) 来只选择直接子链接。

.section-header {
  display: flex;
  justify-content: space-between;
}

/* If there is a link, add the following */
.section-header:has(> a) {
  align-items: center;
  border-bottom: 1px solid;
  padding-bottom: 0.5rem;
}

Card 组件例子1

让我们回顾上面的 card 例子,包含两种情况,一个包含图片而另一个不包含。

.card:has(.card__image) {
    display: flex;
    align-items: center;
}

我们甚至可以为不包含图片的 .card 应用特定的样式,在我们的例子中,就是 border-top

.card:not(:has(.card__image)) {
    border-top: 3px solid #7c93e9;
}

若没有 CSS 的 :has,我们需要两个 CSS 选择器来解决此类问题。

.card--default {
    display: flex;
    align-items: center;
}

.card--plain {
    border-top: 3px solid #7c93e9;
}

Card 组件例子2

在这个例子中,我们有两种情况:一种是只包含链接而另一种包含多个动作(保存、分享等等)。

当 card 动作用两个不同的元素来包裹时,我们希望使用 display:flex 来布局。

<div class="card">
    <div class="card__thumb><img src="cool.jpg"/></div>
    <div class="card__content">
        <div class="card__actions">
            <div class="start">
                <a href="#">Like</a>
                <a href="#">Save</a>
            </div>
            <div class="end">
                <a href="#">More</a>
            </div>
        </div>
    </div>
</div>
.card__actions:has(.start, .end) {
    display: flex;
    justify-content: space-between;
}

下面是没有 CSS 的 :has 时:

.card--with-actions .card__actions {
    display: flex;
    justify-content: space-between;
}

Card 组件例子3

你是否遇到过 Card 组件不包含图片时,需要为内容添加上 border-radius ?这是使用 :has 的绝佳时机。

考虑下面的图例,当图片被移除时左上角、右上角的圆角变为 0,看上去有点奇怪:

/* If no image, add radius to the top left and right corners. */
.card:not(:has(img)) .card__content {
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
}

.card img {
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
}

.card__content {
    border-bottom-left-radius: 12px;
    border-bottom-right-radius: 12px;
}

现在看上去好多了!

没有 :has 我们必须这样做:

.card--plain .card__content {
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
}

过滤组件

在这个例子中,我们有一个包含许多选项的组件,当没有选项被选中时,reset 按钮就不显示。然而至少一个被选中时,reset 按钮需要显示。

我们可以简单的通过 :has 来实现。

.btn-reset {
    display: none;
}

.multiselect:has(input:checked) .btn-reset {
    display: block;
}

根据条件显隐表单元素

我们可能需要根据前一个答案或选择来展示特定的表单字段。在这个例子中,我们一旦选择了 other 选项就需要显示 “other reason” 输入框。

通过 :has 的功能,我们可以检测是否选择了包含 other 选项来展示 “other” 字段。

.other-field {
    display: block;
}

form:has(option[value="other"]:checked) .other-field {
    display: block;
}

是不是很神奇?

拥有子菜单的导航

在这个案例中,我们有一个包含子菜单的导航,hover 或 focus 时展示子菜单。

我们想要的是根据是否包含子菜单来显隐箭头。我们可以通过 CSS :has 来简单的实现,思路是检测 li 是否包含 ul,若有则显示,反之隐藏。

/* Check if the <li> has a <ul>. Yes? show the arrow. */
li:has(ul) > a:after {
    content: "";
    /* arrow styling */
}

若没有 CSS :has ,我们可能需要单独为包含子菜单的 li 添加一个类,像下面这样:

.nav-item--with-sub > a:after {
    content: "";
    /* arrow styling */
}

Header 的包裹容器

当我们有一个 header 组件时,可能需要确定是占满屏幕的宽度或包含在一个容器中。

任何一种方法都需要使用 flexbox 以一种特定的方式来排列子元素。若包含 .wrapper 直接在它上应用样式,反之则需要在 .site-header 上书写样式。

<header class="site-header">
    <div class="wrapper">
        <!-- Header content -->
    </div>
</header>
.site-header:not(:has(.wrapper)) {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-inline: 1rem;
}

/* If it has a wrapper */
.site-header .wrapper {
    display: flex;
    justify-content: space-between;
    align-items: center;
    max-width: 1000px;
    margin-inline: auto;
    padding-inline: 1rem;
}

强调提醒

在一些后台看板中,有一些重要的提示需要用户注意。在那种情况下,单单的提示可能还不够我们可以为头部导航添加浅红色背景,例如:

通过 :has 我们可以检测 .main 是否包含 alert ,然后添加对应的样式。

.main:has(.alert) .header {
    border-top: 2px solid red;
    background-color: #fff4f4;
}

切换颜色方案

我们可以通过 :has 来切换网页颜色,例如,若我们有多种根据 CSS 变量构建的主题,可以通过切换 select 选项实现。

html {
    --color-1: #9e7ec8;
    --color-2: #f1dceb;
}

当我们选择列表中的其他选项时, CSS 将会如下变化:

html:has(option[value="default"]:checked) {
  --color-1: #74559c;
  --color-2: #f9f6fe;
}

html:has(option[value="blueish"]:checked) {
  --color-1: #466ec0;
  --color-2: #ebf0f7;
}

为生成的 HTML 应用样式

一些情况下,我们无法控制生成的 HTML 内容,例如:文章主题,CMS 系统可能会生成意外的内容或作者可能会嵌入视频或其它的。

假设,我们可能希望选中没有 h3 后面没有 p 标签的元素来增加一些空间。

.article-body h3:not(:has(+ p)) {
    margin-bottom: 1.5rem;
}

或者我们想要选中 iframe 后面紧跟 h3 的标签,这些情况没有 :has 是不能完成的!

.article-body h3:has(+ p) {
    /* do something */
}

带图标的按钮

在这个例子中,我们有一个默认的按钮样式。当有图标时,我们期望使用 flexbox 来居中和对齐按钮内容。

.button:has(.c-icon) {
    display: inline-flex;
    justify-content: center;
    align-items: center;
}

多按钮

在一个设计系统中,我们经常会有一组按钮,若按钮数量超过 2 个,最后一个需要放到另一边。

我们可以通过 quantity queries 来实现,下面的样式会检测按钮数量是否多余 3 个,若多则会通过 margin-left:auto 把最后一个元素放到右边。

.btn-group {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
  
.btn-group:has(.button:nth-last-child(n + 3)) .button:last-child {
    margin-left: auto;
}

信息模块

我在 pinterest 中看到一个这样的例子,当有错误时,我们同样需要变更标题来提醒。

.module:has(.input-error) .headline {
    color: #ca3131;
}

依据条目多少来改变 Grid

通过 CSS grid ,我们使用 minmax() 来创建真正的响应式和自适应的 grid 条目。可是,那样可能还不够,我们可能还需要根据条目的多少来设置 grid 。

考虑如下图例。

.wrapper {
    --item-size: 200px;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--item-size), 1fr));
    gap: 1rem;
}

当有 5 个条目时,最后一个会另起一行。

我们可以通过检测 .wrapper 是否包含超过 5个条目,然后使用 quantity queries 的概念。

.wrapper:has(.item:nth-last-child(n + 5)) {
    --item-size: 120px;
}

.wrapper:has(.item:nth-last-child(n + 5)) {
    --item-size: 120px;
}

图例和图例描述

在这个例子中,我们有一个 <figure>,若包含 <figcaption>,样式可能会有点不同:

  • 添加一个白色背景
  • 一些间隙
  • 减少图片的 border-radius
figure:has(figcaption) {
    padding: 0.5rem;
    background-color: #fff;
    box-shadow: 0 3px 10px 0 rgba(#000, 0.1);
    border-radius: 3px;
}

总结:

我迫不及待的想看你们通过 :has 解决什么问题。文章中的用例只是一些基础的!我肯定后面我们会发掘一些有用的用法。

正如大家所说,这是真是一个学习 CSS 的绝佳时机。我对即将到来的新 CSS 属性很兴奋,多谢阅读!

延伸阅读