引言
前端开发人员长期以来的梦想是找到一种方法, 根据元素内部的情况将 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)
选择器会选中所有具有子元素 img
的 p
元素
<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()
没有出来之前:
- 我们都只能够根据
祖先元素
匹配子元素
, 为符合条件的子元素
设置样式, 例如a > img { ... }
它会给a
标签内部的img
设置指定的样式 - 只能够根据
列表中上级元素
匹配下级元素
, 为符合条件的子元素
设置样式, 例如p ~ h2 { ... }
它会给p
标签后面的h2
设置指定的样式 - 只能够根据
祖先元素
的情况、或者前面的兄弟节点
的情况, 来为元素设置样式 - 无法根据
子孙元素
的情况、或后面兄弟节点
的情况, 来为元素设置样式
:has()
又有什么不一样呢?
- 能够根据
子孙元素
的情况, 来为祖先元素
设置样式, 例如a:has(img){ ... }
如果a
标签内存在img
标签, 则给祖先元素a
设置样式 - 能够根据
后面兄弟节点
的情况, 来为前面的兄弟节点
设置样式, 例如p:has(~ h2) { ... }
如果p
标签后面存在兄弟节点h2
, 则给为前面的兄弟节点p
设置样式 - 能够根据页面任意元素的情况, 为页面任意元素设置样式, 例如:
body:has(img) div {...}
如果页面中有img
标签则为所有div
设置样式
那么早期为什么不能够根据 祖先元素
的情况、或者 前面的兄弟节点
的情况, 来为元素设置样式呢? 该需求应该是很常见的, 但一直没有得到支持主要还是考虑到性能的问题, 我们都知道, DOM
的渲染都是从上到下、从内往外的, 试想下如果 子孙元素
能够影响 祖先元素
, 前面的 兄弟元素
能够影响 后面的兄弟元素
, 那么就需要等到加载完 子孙元素
或者所有 后面的兄弟元素
, 然后再回退回去去渲染 祖先元素
或者 前面的兄弟节点
, 这必然会影响网页的渲染速度!!!
其实 :has()
伪类的规范制定得很早, 但是却一直没有得到支持!! 浏览器厂商主要还是顾忌性能的影响, 那么现在 :has()
又为什么能够被推广使用咯? 这里还是得感谢 Igalia, Igalia
是一家著名的私营软件咨询公司, 专注于开源软件!! JS
CSS
中很多新特性都是它们推出的! 本文 :has()
就是该公司的另一个作品, 它解决搞定了浏览器几十年都无法解决的性能问题!!! 👏🏻👏🏻👏🏻👏🏻
1.3 注意
:has()
支持复杂的选择器列表, 选择器列表之间的关系是或
的关系, 如下代码:article:has(h2, h3)
将匹配到article
中存在h2
或h3
的article
元素, 其实:has()
中的选择器列表等价于h2,h3
<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>
最后效果如下:
- 那么如果希望使用
与
的关系要怎么做呢? 这里其实可以使用两个:has()
, 如下代码所示: 只有同时存在h2
和h3
的article
才会被匹配到
<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>
最后效果如下:
- 如果选择器列表包含一个无效的选择器, 那么整个列表将被忽略, 如下代码所示
::-blahdeath
是无效的选择器, 那么真该条规则将被忽略(不生效)
article:has(h2, ul, ::-blahdeath) {
/* ::blahdeath 是无效的, 所以整个规则将失效 */
}
那么解决方法也简单, 就是嵌套一个更宽容的选择器, 例如 :is()
或者 :where()
, 如下代码所示, 对选择器列表包了一层 :is()
, 整条规则将正常生效
article:has(:is(h2, ul, ::-blahdeath)) {
/* :is 更宽容, 允许参数是一个无效的选择器 */
}
- 兼容写法, 使用
@supports
只对支持:has()
的浏览器生效
@supports(selector(:has(p))) {
/* 支持! */
}
- 最后看下到目前(
2023.10.11
)为止:has()
的兼容性
二、三个场景
2.1 根据「子孙元素」匹配「祖先元素」
如题, 可根据子孙元素的一个具体情况, 为祖先元素设置不同的样式, 如下代码所示:
- 三个
div
, 第一个div
存在一个孙节点img
, 第二个div
存在一个子节点img
, 第三个则不包含div
- 通过
div:has(img)
获取到所有子节点、或者孙节点存在img
的div
标签 - 通过
div:not(:has(img))
则获取到所有子节点、或者孙节点没有img
的div
标签, 其实就是div:has(img)
取反
<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
的情况, 这里可以使用 >
选择器来实现
<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
节点
<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
内的任意元素设置样式
<style>
body:has(.box1 p:hover) .box2 p::after {
color: #f759ab;
content: 'box1 被 hover 咯!!!';
}
</style>
<div class="box2"><p> </p></div>
<div class="box1">
<p>在五千多年中华文明</p>
<p>深厚基础上开辟和发展中国特色社会主义</p>
<p>把马克思主义</p>
</div>
代码效果如下: 当鼠标 hover
到 .box1
内的 p
标签, 将会通过伪元素(:after
)为 .box2 p
添加内容(box1 被 hover 咯!!!
)
三、实战
3.1 表单必填设置
如下代码, 通过 :has
配合为伪类、伪元素, 可给需要的表单设置必填标记
<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
显示则表示表单未填写, 否则表示表单已填写
<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
的行高得属性, 来进行文本的编排
<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()
设置body
的CSS
变量, 完成页面主题的切换 - 页面通过实现了一个常见的卡片布局, 并调用
CSS
变量来完成页面的绘制, 目的是为了演示CSS
主题切换的效果
<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>
最后效果如下: 通过下拉框切换主题, 改变卡片的整体配色
四、参考
- 来了, 来了, CSS :has() 伪类她来了
- MDN - :has()
- CSS最新的強大 :has() 父層選取器來了!
- The CSS :has Selector (and 4+ Examples)
- Parent Selectors in CSS
- Using :has() as a CSS Parent Selector and much more
- CSS 选择器的一场革命,:has() 高级使用指南