CSS 有了: has 僞類可以做些什麼?

無比強大的: has 僞類終於來了~,一起看看吧

相信大家最近對:has都有所耳聞,規範提及了那麼久,卻遲遲沒有動靜,最近瀏覽器終於開始支持了~🎉🎉

:has僞類是一個非常強大的僞類,強大到難以置信,可以做很多夢寐以求的事情,很多以前只能更改 dom 結構 或者只能用 JS 才能實現的功能現在也能純 CSS 實現了,一起看看吧

一、簡單介紹一下:has

:has僞類的語法非常簡單,表示滿足一定條件後,就會匹配該元素。

例如,下面的選擇器只會匹配直接包含img子元素的a元素:

a:has(> img)

再例如,下面的選擇器只會匹配其後緊跟着p元素的h1元素

h1:has(+ p)

以我個人的理解來看,去除:has()後,剩下的選擇器仍然是完整的

a>img

加上:has()後,可以選中最前面的元素a

好了,語法其實就這麼多,估計沒啥吸引力,關鍵是實際應用。下面通過幾個實例來感受一下:has僞類的強大魅力~

溫馨提醒:兼容性要求需要 Chrome 101+,並且開始實驗特性(105 + 正式支持),Safari 15.4+,Firefox 官方說開啓實驗特性可以支持,但是實測並沒有(???)

二、表單元素必填項

先來看一個簡單例子,下面有一個表單元素,有一些是必填項

<form>
  <item>
    <label>用戶名</label>
    <input required>
  </item>
  <item>
    <label>備註</label>
    <input>
  </item>
</form>

image-20220903120206213

現在可以通過:has在必填項的前面加上紅色的星號

label:has(+input:required)::before{
  content: '*';
  color: red;
}

這個應該還比較好理解,通過:has+可以選中滿足條件的label,然後再生成::before僞元素。如果是在以前,可能需要手動添加類名,或者改變html的書寫順序

image-20220903120438968

你也可以訪問線上完整 demo:has+required(codepen.io)[1] 或者 has+required(runjs.work)[2]

三、拖拽指定區域

有些時候列表需要有拖拽功能,但爲了拖拽體驗,不影響列表內部操作,可能需要指定小部分區域可拖拽,例如

HTML 結構如下

<div class="content">
  <div class="item">列表<span class="thumb"></span></div>
  <div class="item">列表<span class="thumb"></span></div>
  <div class="item">列表<span class="thumb"></span></div>
</div>

現在我們希望在hover時出現拖拽手柄,按住拖拽手柄纔可以拖拽,看着好像非常麻煩,但是現在藉助:has僞類可以輕易實現,關鍵 CSS 如下

.thumb{
  /**/
  opacity: 0
}
.item:hover .thumb{
    opacity: 1;
}
.item:has(.thumb:hover){
    -webkit-user-drag: element;
}

這裏的:has表示當.thumb處於:hover狀態時選中該元素,從而給.item添加可拖拽屬性,效果如下

你也可以訪問線上完整 demo:drag_thumb(codepen.io)[3] 或者 drag_thumb(runjs.work)[4]

四、多層級 hover

再來看一個例子,早在四年前我就提到過的 CSS 多層級 hover 問題 [5],現在終於有解了~

是這樣的,有一個多層級的結構,例如

<div class="box-1">
  <div class="box-2">
    <div class="box-3"></div>
  </div>
</div>

如果給div添加hover樣式

div:hover{ 
  outline:4px dashed rebeccapurple
}

效果是這樣

可以看到,當hover到裏層元素時,外層元素也觸發了hover樣式。這有點像JS中的冒泡效果,那如何讓hover的時候只觸發當前的元素呢?也就是排除掉他的父級元素,沒錯,:has可以很好的解決這個問題

div:not(:has(:hover)):hover{ 
  outline:4px dashed rebeccapurple
}

是不是越來越繞了?別急,我們拆解分析一下,div:has(:hover)表示有子元素正處於hoverdiv,比如當hoverbox-3時,div:has(:hover)選中的就是除box-3以外的兩個父級,然後加上:not就剛好反過來,只選中box-3本身,可以理解嗎?這個可以下來多試試,實際效果如下

你也可以訪問線上完整 demo:CSS hover(codepen.io)[6] 或者 CSS hover(runjs.work)[7]

在一些可視化拖拽平臺,各種嵌套的組件中會很有作用

五、評星組件

這個功能也是非常適合用:has來實現的,HTML 結構如下

<star>
  <input >
  <input >
  <input >
  <input >
  <input >
</star>

簡單修飾一下

star{
  display: flex;
}
star [type="radio"]{
  appearance: none;
  width: 40px;
  height: 40px;
  margin: 0;
  cursor: pointer;
  background: #ccc;
  transition: .3s;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'%3E%3C/path%3E %3C/svg%3E") center / 80% no-repeat;
}

效果如下

下面要做交互功能,當:hover或者:checked時,當前元素和當前元素之前的元素都觸發選中。

在之前,由於只有後置兄弟選擇器~,所以必須要將 dom 元素更改順序,然後通過其他方式在視覺上再翻轉過來。現在有了:has,這些奇技淫巧都可以說拜拜了,實現如下

star [type="radio"]:hover,
star [type="radio"]:has(~:hover),
star:not(:hover) [type="radio"]:checked,
star:not(:hover) [type="radio"]:has(~:checked){
  background: orangered;
}

相信不算太複雜,[type="radio"]:has(~:hover)表示選中當前hover元素之前的元素,所以可以輕易的實現評分的效果

你也可以訪問線上完整 demo:CSS star (codepen.io)[8] 或者 CSS rate(runjs.work)[9]

六、日期範圍選擇

如果說上面這些例子有其他代替方案,或者說用一點點 JS 也能實現,那下面來一個重磅級的案例。在以前,就算靠 JS 也會有一些麻煩,但是有了:has,一切都變得簡單了~

假設 HTML 結構如下

<div class="date">
  <span>1</span>
  <span>2</span>
  <span>3</span>
  ...
  <span>30</span>
  <span>31</span>
</div>

這部分交互有兩個部分,一個是鼠標滑過,還有一個是選中。

我們先看選中的功能,當有兩個元素被選中時,這兩個元素之間的元素都會匹配上,假設選中的類名是select

<div class="date">
  <span>1</span>
  <span>2</span>
  <span class="select">3</span>
  ...
  <span class="select">30</span>
  <span>31</span>
</div>

那麼,如何讓這一片區域的元素都匹配上呢?答案就是通過:has找到select之前的元素,再結合~匹配之後的元素,兩者結合就可以匹配到中間的元素了,具體實現如下

.select,
.select~span:has(~.select){
   background-color: blueviolet;
   color: #fff;
}

效果如下

然後是 hover 的效果,假設有一個已經被選中了

<div class="date">
  <span>1</span>
  <span>2</span>
  <span class="select">3</span>
  ...
  <span>30</span>
  <span>31</span>
</div>

現在需要再鼠標滑過的時候,將鼠標的終點和已選中的範圍都匹配上,這個就稍微有些複雜了,我們需要考慮鼠標在已選中元素之前還是之後,分別用:has進行判斷,實現如下

span:hover~span:has(~.select),
.select~span:has(~:hover)
{
  background-color: blueviolet;
  color: #fff;
}

是不是有些暈了?第一條表示鼠標在已選中之前,匹配當前hover之後、.selelct之前的元素, 第二條表示已選中之後,匹配.selelct之後、hover之前的元素,實際效果如下

還有一個問題,需要區分一下選中兩個和只選中一個的情況,因爲兩個表示區間選擇已經完成,此時hover不會有效果,藉助:has僞類,可以很輕易的區分子元素的個數,如下

.date:not(:has(.select~.select)){
  /*匹配到沒有兩個.select的父級*/
}

.select~.select表示選中.select後面的.select,也就是表示至少有兩個.select,然後通過:has就可以區分這兩種情況了

.date:not(:has(.select~.select)) .select,
.date:not(:has(.select~.select)) span:hover{
  background-color: transparent;
  color: inherit;
  outline: 2px solid blueviolet;
  outline-offset: -2px;
}

.date:not(:has(.select~.select)) span:hover~span:has(~.select),
.date:not(:has(.select~.select)) .select~span:has(~:hover)
{
  background-color: blueviolet;
  color: #fff;
}

元素的選中時通過JS實現的,這時候的JS完全就只是工具人了,和視覺完全不相干,只需要記錄選中的元素,邏輯極其簡單,如下

date.addEventListener('click'ev ={
  const current = date.querySelectorAll('.select');
  if (current.length == 2) {
    current.forEach(el ={
      el.classList.remove('select')
    })
  }
  ev.target.classList.add('select')
})

然後就可以得到這樣的效果了

你也可以訪問線上完整 demo:CSS date-range(codepen.io)[10] 或者 CSS date-range(runjs.work)[11]

等待時機成熟,xy-ui 中的 date-picker[12] 也會跟進使用這一方案~

七、別等了,現在就學起來

以上就是我在研究:has僞類後,第一時間所想到的一些應用場景,這麼好用的僞類,還不學一下嗎?

當然其作用遠不止這些,可以這麼講,以前需要考慮 dom 順序的場景都可以解決了,以後的 dom 將更加語義化,大部分交互狀態僞類都可以很好地結合在一起,80% 以上交互相關的 JS 代碼都可以去掉了,JS 可以更加安心的去做自己該做的事情了,比如數據轉換,業務邏輯等等。

參考資料

[1]

has+required(codepen.io): https://codepen.io/xboxyan/pen/yLjyopY

[2]

has+required(runjs.work): https://runjs.work/projects/5819b7bc43474966

[3]

drag_thumb(codepen.io): https://codepen.io/xboxyan/details/QWrwMaL

[4]

drag_thumb(runjs.work): https://runjs.work/projects/502506f18ee34b6e

[5]

CSS 多層級 hover 問題: https://segmentfault.com/q/1010000016225343/a-1020000042380822

[6]

CSS hover(codepen.io): https://codepen.io/xboxyan/pen/eYrmEyd

[7]

CSS hover(runjs.work): https://runjs.work/projects/2a7e819e473c47af

[8]

CSS star (codepen.io): https://codepen.io/xboxyan/pen/abGzyEw

[9]

CSS rate(runjs.work): https://runjs.work/projects/b1ad1cf84b314dfd

[10]

CSS date-range(codepen.io): https://codepen.io/xboxyan/details/dyePzVd

[11]

CSS date-range(runjs.work): https://runjs.work/projects/85ba26f3ff18414b

[12]

date-picker: https://xy-ui.codelabo.cn/docs/#/xy-date-picker

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qVfdHy8QjZFVjEscAZQYXw