:has(): 根据「页面任意元素状态」为「任意元素」设置「不同样式」

引言

前端开发人员长期以来的梦想是找到一种方法, 根据元素内部的情况将 CSS 应用到该元素!!! 这类场景算是很常见的了, 比如:

  • 根据容器内部是否具有图片, 来为容器设置不同的布局、宽高、背景等等等等
  • 或者在表单中, 我们可能需要根据每个表单的状态、填写情况... 来为表单设置不同的样式
  • 又或者根据某个组件是否存在, 来给父元素或页面上任意元素设置不同的样式

总之根据子孙元素的情况, 为祖先元素设置样式; 或者根据后面兄弟节点的情况, 为前面的兄弟节点设置样式; 这一类的需求是司空见惯的, 在早期我们往往都是需要借助 JS 来实现; 好在现在出现了 :has(), 终于可以不用 JS 来实现该类需求咯, 当然实际上 :has() 更为强大, 它可以让你根据页面任意元素的情况, 为页面任意样式设置样式, 那么下面开始...

一、简介

1.1 是什么

首先 :has()CSS 中的一个函数式伪类, 它的参数是一组 CSS 选择器列表, 如: :has(img, + p, :is(h1, h2, h3))

伪类 :has(selectorList)selectorList 是要用来匹配子元素的一组选择器列表, 而整个 :has(selectorList) 选择器会选中包含符合 selectorList 规则的子元素的 父元素, 举个例子, 如下代码: p:has(img) 选择器会选中所有具有子元素 imgp 元素

html 复制代码
<style>
  p:has(img) {
    background-color: #ccc;
  }
</style>
<p>11111 <img src="" alt=""></p>
<p>22222</p>

最后效果如下: 因为第一个 p 元素中存在 img 标签, 所以将被 p:has(img) 选择器匹配到

1.2 特别之处

:has() 没有出来之前:

  1. 我们都只能够根据 祖先元素 匹配 子元素, 为符合条件的 子元素 设置样式, 例如 a > img { ... } 它会给 a 标签内部的 img 设置指定的样式
  2. 只能够根据 列表中上级元素 匹配 下级元素, 为符合条件的 子元素 设置样式, 例如 p ~ h2 { ... } 它会给 p 标签后面的 h2 设置指定的样式
  3. 只能够根据 祖先元素 的情况、或者 前面的兄弟节点 的情况, 来为元素设置样式
  4. 无法根据 子孙元素 的情况、或 后面兄弟节点 的情况, 来为元素设置样式

:has() 又有什么不一样呢?

  1. 能够根据 子孙元素 的情况, 来为 祖先元素 设置样式, 例如 a:has(img){ ... } 如果 a 标签内存在 img 标签, 则给祖先元素 a 设置样式
  2. 能够根据 后面兄弟节点 的情况, 来为 前面的兄弟节点 设置样式, 例如 p:has(~ h2) { ... } 如果 p 标签后面存在兄弟节点 h2, 则给为前面的兄弟节点 p 设置样式
  3. 能够根据页面任意元素的情况, 为页面任意元素设置样式, 例如: body:has(img) div {...} 如果页面中有 img 标签则为所有 div 设置样式

那么早期为什么不能够根据 祖先元素 的情况、或者 前面的兄弟节点 的情况, 来为元素设置样式呢? 该需求应该是很常见的, 但一直没有得到支持主要还是考虑到性能的问题, 我们都知道, DOM 的渲染都是从上到下、从内往外的, 试想下如果 子孙元素 能够影响 祖先元素, 前面的 兄弟元素 能够影响 后面的兄弟元素, 那么就需要等到加载完 子孙元素 或者所有 后面的兄弟元素, 然后再回退回去去渲染 祖先元素 或者 前面的兄弟节点, 这必然会影响网页的渲染速度!!!

其实 :has() 伪类的规范制定得很早, 但是却一直没有得到支持!! 浏览器厂商主要还是顾忌性能的影响, 那么现在 :has() 又为什么能够被推广使用咯? 这里还是得感谢 Igalia, Igalia 是一家著名的私营软件咨询公司, 专注于开源软件!! JS CSS 中很多新特性都是它们推出的! 本文 :has() 就是该公司的另一个作品, 它解决搞定了浏览器几十年都无法解决的性能问题!!! 👏🏻👏🏻👏🏻👏🏻

1.3 注意

  1. :has() 支持复杂的选择器列表, 选择器列表之间的关系是 的关系, 如下代码: article:has(h2, h3) 将匹配到 article 中存在 h2h3article 元素, 其实 :has() 中的选择器列表等价于 h2,h3
html 复制代码
<style>
  article:has(h2, h3) {
    background-color: #fff1f0;
  }
</style>

<article>
  <h2>Default title h2</h2>
  <p>content h2</p>
</article>

<article>
  <h3>Default title h3</h3>
  <p>content h3</p>
</article>

<article>
  <h2>Default title h2</h2>
  <h3>Default title h3</h3>
  <p>content h2 h3</p>
</article>

最后效果如下:

  1. 那么如果希望使用 的关系要怎么做呢? 这里其实可以使用两个 :has(), 如下代码所示: 只有同时存在 h2h3article 才会被匹配到
diff 复制代码
<style>
+ article:has(h2):has(h3) {
    background-color: #fff1f0;
  }
</style>

<article>
  <h2>Default title h2</h2>
  <p>content h2</p>
</article>

<article>
  <h3>Default title h3</h3>
  <p>content h3</p>
</article>

<article>
  <h2>Default title h2</h2>
  <h3>Default title h3</h3>
  <p>content h2 h3</p>
</article>

最后效果如下:

  1. 如果选择器列表包含一个无效的选择器, 那么整个列表将被忽略, 如下代码所示 ::-blahdeath 是无效的选择器, 那么真该条规则将被忽略(不生效)
css 复制代码
article:has(h2, ul, ::-blahdeath) {
  /* ::blahdeath 是无效的, 所以整个规则将失效 */
}

那么解决方法也简单, 就是嵌套一个更宽容的选择器, 例如 :is() 或者 :where(), 如下代码所示, 对选择器列表包了一层 :is(), 整条规则将正常生效

css 复制代码
article:has(:is(h2, ul, ::-blahdeath)) {
  /* :is 更宽容, 允许参数是一个无效的选择器 */
}
  1. 兼容写法, 使用 @supports 只对支持 :has() 的浏览器生效
css 复制代码
@supports(selector(:has(p))) {
  /* 支持! */
}
  1. 最后看下到目前(2023.10.11)为止 :has() 的兼容性

二、三个场景

2.1 根据「子孙元素」匹配「祖先元素」

如题, 可根据子孙元素的一个具体情况, 为祖先元素设置不同的样式, 如下代码所示:

  • 三个 div, 第一个 div 存在一个孙节点 img, 第二个 div 存在一个子节点 img, 第三个则不包含 div
  • 通过 div:has(img) 获取到所有子节点、或者孙节点存在 imgdiv 标签
  • 通过 div:not(:has(img)) 则获取到所有子节点、或者孙节点没有 imgdiv 标签, 其实就是 div:has(img) 取反
html 复制代码
<style>
  div:not(:has(img)) {
    width: 400px;
    background-color: red;
  }

  div:has(img) {
    width: 600px;
    background-color: #ccc;
  }
</style>
<div>
  <p>2023 年年初</p>
  <p>
    随着西非第一大深水港
    <img src="" alt="">
  </p>
</div>
<div>
  <img src="" alt="">
  <p>由中企承建的尼日利亚莱基港正式开港运营</p>
</div>
<div>
  <p>非洲第一大经济体</p>
  <p>终于结束了多年没有深水港的历史</p>
</div>

效果如下: 对于包含 img 的容器, 将设置不同的背景色、并且设置不同的宽度

下面我们调整下需求: 只匹配子元素存在 img 的情况, 这里可以使用 > 选择器来实现

diff 复制代码
<style>
+ div:not(:has(> img)) {
    width: 400px;
    background-color: red;
  }

+ div:has(> img) {
    width: 600px;
    background-color: #ccc;
  }
</style>
<div>
  <p>2023 年年初</p>
  <p>
    随着西非第一大深水港
    <img src="" alt="">
  </p>
</div>
<div>
  <img src="" alt="">
  <p>由中企承建的尼日利亚莱基港正式开港运营</p>
</div>
<div>
  <p>非洲第一大经济体</p>
  <p>终于结束了多年没有深水港的历史</p>
</div>

最后效果如下:

2.2 根据「后面的兄弟元素」匹配「前面的兄弟元素」

我们来做个需求: 有一个列表, 当鼠标 hover 到某一行时, 背景色设置为绿色, 该行前面的所有项背景色设为黄色, 后面的背景色则设置为蓝色

如下代码:

  • p:hover 选中鼠标 hover 的那个 p 标签
  • p:hover ~ p 则选择鼠标 hover 的那个 p 标签, 后面的所有兄弟节点 p
  • p:has(~ p:hover) 则是选中所有「后面有兄弟节点被鼠标 hover」的 p 节点
html 复制代码
<style>
  p {
    margin: 0;
    cursor: pointer;
    padding: 2px 10px;
    transition: all 0.4s;
  }

  p:has(~ p:hover) {
    background-color: #ffc53d;
  }

  p:hover {
    background-color: #73d13d;
  }

  p:hover ~ p {
    background-color: #597ef7;
  }
</style>

<div>
  <p>在五千多年中华文明</p>
  <p>深厚基础上开辟和发展中国特色社会主义</p>
  <p>把马克思主义</p>
  <p>基本原理同中国具体实际</p>
  <p>同中华优秀传统文化 </p>
  <p>相结合是必由之路</p>
</div>

最后效果如下:

2.3 根据任意元素状态, 匹配页面任意元素

如题, 通过 :has() 并配合其他选择器, 我们可以根据页面上任意元素的状态, 匹配到任意我们想要匹配的元素, 如下代码所示:

  • 选择器 body:has(.box1 p:hover) 如果 body 中存在能够被 .box1 p:hover 匹配的元素, 则 body 会被选中
  • 然后再配合其他选择器, 为 body 内的任意元素设置样式
html 复制代码
<style>
  body:has(.box1 p:hover) .box2 p::after {
    color: #f759ab;
    content: 'box1 被 hover 咯!!!';
  }
</style>

<div class="box2"><p>&nbsp;</p></div>
<div class="box1">
  <p>在五千多年中华文明</p>
  <p>深厚基础上开辟和发展中国特色社会主义</p>
  <p>把马克思主义</p>
</div>

代码效果如下: 当鼠标 hover.box1 内的 p 标签, 将会通过伪元素(:after)为 .box2 p 添加内容(box1 被 hover 咯!!!)

三、实战

3.1 表单必填设置

如下代码, 通过 :has 配合为伪类、伪元素, 可给需要的表单设置必填标记

html 复制代码
<style>
  label:has(+ input:required)::before{
    content: '*';
    color: red;
  }
</style>
<form>
  <item>
    <label>用户名</label>
    <input required>
  </item>
  <item>
    <label>备注</label>
    <input>
  </item>
</form>

最后效果如下:

3.2 必填项校验

需求: 根据表单必填项的填写情况设置表单容器的样式

如下代码所示:

  • 四个表单, 两个必填
  • form:has(input:required:placeholder-shown): 如果表单 form 存在必填表单没有填写, 将匹配到 form 节点, 其中 :placeholder-shown 伪类, 可匹到 placeholder 显示状态下的表单, 如果 placeholder 显示则表示表单未填写, 否则表示表单已填写
html 复制代码
<style>
  label:has(+ input:required)::before{
    content: '*';
    color: red;
  }

  label {
    width: 4em;
    text-align: right;
    display: inline-block;
  }

  item:not(:last-child) {
    margin-bottom: 10px;
  }

  form {
    padding: 20px;
    display: inline-flex;
    flex-direction: column;
    
    border-radius: 4px;
    background-color: #f6ffed;
    border: 1px solid #73d13d;

    transition: all 0.4s;
  }

  form:has(input:required:placeholder-shown) {
    border-color: #f759ab;
    background-color: #ffd6e7;
  }
</style>
<form>
  <item>
    <label>用户名</label>
    <input required placeholder="请输入用户名">
  </item>
  <item>
    <label>备注</label>
    <input placeholder="请输入备注">
  </item>
  <item>
    <label>年龄</label>
    <input required placeholder="请输入年龄">
  </item>
  <item>
    <label>简介</label>
    <input placeholder="请输入简介">
  </item>
</form>

最后效果如下: 如下图, 在必填项未全部填写完整情况下, 整个表单背景色、边框会显示红色, 全部填写完整后, 背景色和边框将显示绿色

3.3 文本编排

如下代码所示: 根据紧跟 h1~h6 标签后面的元素情况, 设置 h1~h6 的行高得属性, 来进行文本的编排

css 复制代码
<style>
  h1, 
  h2,
  h3, 
  h4, 
  h5,
  h6 {
    margin: 0;
    line-height: 1.5em;
  }

  .header-group {
    margin: 10px;
    padding: 10px;
    background-color: #e6fffb;
  }

  .header-group :is(h1, h2, h3, h4, h5, h6):has( + .subtitle) {
    line-height: 1.2em;
  }

  .header-group :is(h1, h2, h3, h4, h5, h6):has( + :is(h1, h2, h3, h4, h5, h6)) {
    line-height: 1.2em;
  }
</style>

<div class="header-group">
  <h2>Blog Post Title</h2> 
  <p>文本编排</p>
</div>

<div class="header-group">
  <h2>Blog Post Title</h2>
  <div class="subtitle"> 
    This is a subtitle
  </div>
  <p>文本编排</p>
</div>

<div class="header-group">
  <h1>Blog Post Title</h1>
  <h2>Blog Post Title</h2>
  <p>文本编排</p>
  <h3>Blog Post Title</h3>
  <p>文本编排</p>
</div>

最后效果如下:

3.4 主题切换

:has() 配合 CSS 变量, 可轻松实现页面上主题的切换, 如下代码所示:

  • body 中定义了一套默认的 CSS 变量, 包括字体颜色、背景色、边框颜色...
  • 页面通过 select 可进行主题的切换, 通过 option[value="cyan"]:checked 可匹配 cyan 选项选中的状态, 并配合 :has() 设置 bodyCSS 变量, 完成页面主题的切换
  • 页面通过实现了一个常见的卡片布局, 并调用 CSS 变量来完成页面的绘制, 目的是为了演示 CSS 主题切换的效果
HTML 复制代码
<style>
  /* 默认 */
  body {
    --text-color: #092b00;
    --box-border-color: #73d13d;
    --header-background: #d9f7be; 
    --content-background: #f6ffed;
  }

  /* 明青 */
  body:has(option[value="cyan"]:checked) {
    --text-color: #002329;
    --box-border-color: #36cfc9;
    --header-background: #b5f5ec; 
    --content-background: #e6fffb;
  }

  /* 法式洋红 */
  body:has(option[value="magenta"]:checked) {
    --text-color: #520339;
    --box-border-color: #f759ab;
    --header-background: #ffd6e7; 
    --content-background: #fff0f6;
  }

  .card {
    width: 400px;
    margin-bottom: 20px;
    
    overflow: hidden;
    border-radius: 4px;
    color: var(--text-color);
    border: 1px solid var(--box-border-color);
  }

  .header {
    font-size: 16px;
    font-weight: 600;
    padding: 2px 10px;
    background-color: var(--header-background);
  }

  .content {
    padding: 10px;
    font-size: 13px;
    background-color: var(--content-background);
  }
</style>

<div class="card">
  <div class="header">Default size card</div>
  <div class="content">
    <p>Card content</p>
    <p>Card content</p>
    <p>Card content</p>
  </div>
</div>

<select name="pets" id="pet-select">
  <option value>--请选择主题--</option>
  <option value="cyan">明青</option>
  <option value="magenta">法式洋红</option>
</select>

最后效果如下: 通过下拉框切换主题, 改变卡片的整体配色

四、参考

相关推荐
For. tomorrow2 分钟前
Vue3中el-table组件实现分页,多选以及回显
前端·vue.js·elementui
.生产的驴14 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
布瑞泽的童话28 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
白鹭凡41 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_862886781 小时前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
书中自有妍如玉1 小时前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn1 小时前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f8979070701 小时前
layui 可以使点击图片放大
前端·javascript·layui
小贵子的博客1 小时前
ElementUI 用span-method实现循环el-table组件的合并行功能
javascript·vue.js·elementui