前端圈子里最近都在聊这个叫:has()的新玩意儿,说真的,这货简直是CSS界的“反向操作大师”。以前写选择器只能从上往下找孩子,现在好了,直接能通过孩子反查老爸,甚至能查到更远的亲戚。拿个实际场景打个比方:就像家里装了监控,不用再喊“谁在客厅”,直接通过客厅里那盏亮着的灯就知道有人在那。:has()就是那盏灯的信号,让样式表能根据某个元素内部有没有特定东西来改变自己的样子。
写样式的时候经常碰上一类情况:一个组件的行为得影响页面另一块的外观。比如点开导航菜单,顶栏颜色得跟着变。以前得靠JS去翻DOM树,手动加类名,代码又臭又长。现在
:has()直接让CSS自己搞定,省下一大堆脚本操作。下面聊聊几个真实项目里能立刻用上的骚操作。
组件内外联动
搞过React或者Vue的兄弟都懂,一个组件想改变它外面容器的样式,通常得搞个全局状态,或者直接用document.querySelector去硬改。拿一个巨幕菜单来说,菜单一展开,上方的<header>背景色就得深一点。旧路子是这样的:
// 组件内部点击时
const menu = document.querySelector('.megamenu');
const header = document.querySelector('header');
menu.addEventListener('open', () => {
header.classList.add('dark-bg');
});这写法不算错,但总感觉在优雅的CSS里掺了沙子。现在换个思路:
/* 只要header里面藏着展开的菜单,就自动变样 */
header:has(.megamenu--open) {
background-color: #1a1a1a;
transition: background 0.2s;
}菜单组件自己管好自己的megamenu--open类就行,外面的事全扔给CSS。这里得提个醒,:has()里传的选择器越具体,整个规则的权重就越高。如果后面想覆盖这个背景色,得用同样强或者更强的选择器才行。要是想保持低调,可以套一层:where(),把权重清零:
header:where(:has(.megamenu--open)) {
background-color: #1a1a1a;
}这样写,外面随便一个类就能轻松覆盖,省得跟权重打架。
表格条纹按需显示
表格加斑马纹是好习惯,但表格只有两三行的时候,条纹反而显得多余。就像三个人排队,非得给第二个穿个花衣服,旁边的人会纳闷“他凭啥特殊”。不如定个规矩:行数超过五条,才开启条纹模式。
/* 当tbody里至少躺着5个tr,再给偶数行加底色 */
tbody:has(tr:nth-child(5)) tr:nth-child(even) {
background-color: #f0f0f0;
}逻辑是这样的::has(tr:nth-child(5))检查表格主体里有没有第五行。有第五行,说明总数≥5,然后给所有偶数行上色。没有第五行,整个规则直接失效,表格干干净净。还可以更骚一点,根据列数来决定样式:
/* 至少有三列,才显示条纹 */
table:has(:is(td, th):nth-child(3)) tbody tr:nth-child(even) {
background-color: #f0f0f0;
}:is(td, th):nth-child(3)匹配到第三个格子,就说明列数够三。这种写法特别适合那种动态列的数据表格,不用JS去数数,CSS自己判断。
模板里甩掉条件类名
CMS建站的时候,侧边栏有没有,直接决定主内容区的布局。以前的做法是在模板里判断:
<div class="standard-page <?php if($hasSidebar) echo 'with-sidebar'; ?>">
<main>...</main>
<?php if($hasSidebar) echo '<aside>...</aside>'; ?>
</div>然后CSS写两个版本:
.standard-page.with-sidebar {
display: grid;
grid-template-areas: 'side side main main main main';
}
.standard-page:not(.with-sidebar) {
grid-template-areas: '. main main main main main .';
}模板里塞一堆逻辑,看着乱。换成:has()之后:
/* 自动检测有没有.sidebar元素 */
.standard-page:has(.sidebar) {
grid-template-areas: 'side side main main main main';
}
.standard-page:not(:has(.sidebar)) {
grid-template-areas: '. main main main main main .';
}HTML里只需要干净地放上.standard-page和可能存在的.sidebar,CSS自己决定怎么摆。这里有个小坑::not(:has(...))的写法,括号千万别乱套。写成:not(:has(.sidebar))意思是“不包含侧边栏”,而:has(:not(.sidebar))意思就完全变了,变成“包含一个不是侧边栏的东西”,那几乎所有元素都满足,直接翻车。
特异性的精细化控制
:has()的权重取决于参数里最猛的那个选择器。举个例子:
/* 权重 = 1个ID + 1个类,非常猛 */
.article:has(#special-section) {
border-color: red;
}这个规则几乎没人能覆盖,除非再用一个带ID的。但大多数时候不需要这么生猛。可以用:where()来降权:
/* 权重 = 0,乖得很 */
.article:where(:has(#special-section)) {
border-color: red;
}:where()的权重永远是0,套在外面之后,整个规则就只剩下.article这一个类的权重(0,1,0)。随便来个.article.highlight就能压过去。日常开发推荐这种写法,免得日后维护的时候发现怎么也改不动样式,最后只能上!important,那可就真的裂开了。
说来说去,:has()就像给CSS装上了后视镜,能看到后面的路况再做反应。不管是改布局、加条纹、还是联动组件,都能少写一堆又臭又长的JS。等火狐一跟上,这玩意儿就能全面铺开,到时候前端工地上的活又能轻松一截。
