歡迎關注我的公眾號:前端偵探
前不久在這篇文章:CSS 有了:has偽類可以做些什麼 中介紹了:has
偽類的一些使用場景,可以說大大顛覆了 CSS 選擇器的認知,讓很多繁瑣的 js
邏輯透過靈活的CSS
輕易實現了。這次帶來一個比較常見的案例,3d 輪播圖,就像這樣的
這個輪播圖有幾個需要實現的點:
- 3d 視覺,也就是中間大,兩邊小
- 自動輪播,滑鼠放上自動暫停
- 點選任意卡片會立即跳轉到該卡片
這次藉助:has
來實現這樣的功能,相信可以帶來不一樣的思路,一起看看吧
⚠️溫馨提醒:相容性要求需要 Chrome 101+,並且開始實驗特性(105+正式支援),Safari 15.4+,Firefox 官方說開啟實驗特性可以支援,但是實測並不支援
一、3d 視覺樣式
這一部分才是重點。
首先我們來簡單佈局一下,假設HTML
是這樣的:
<div class="view" id="view">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item current">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
<div class="item">10</div>
</div>
為了儘可能減少複雜度,我們可以將所有的變化都集中在一個類名上。
比如.current
表示當前選中項,也就是中間的那項。這裡採用絕對定位的方式,將所有卡片項都堆疊在一起,然後設定3d
檢視,將卡片的Z
軸往後移動一段距離,讓.current
在所有卡片的最前面,實現近大遠小的效果,具體實現如下
.view {
position: relative;
width: 400px;
height: 250px;
transform-style: preserve-3d;
perspective: 500px;
}
.item {
position: absolute;
width: 100%;
height: 100%;
border-radius: 8px;
display: grid;
place-content: center;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
transform: translate3d(0, 0, -100px);
}
.item.current {
transform: translate3d(0, 0, 0);
}
效果如下
其實層級是這樣的,可以在 Chrome 圖層中看到
現在,我們需要讓相鄰左右兩邊的都漏出來,右邊的比較容易,用相鄰兄弟選擇器+
就可以了
.item.current + .item{
transform: translate3d(30%, 0, -100px);
}
那相鄰左邊的呢?以前是無解的,只能透過 JS 分別設定不同的類名,非常麻煩,但現在有了:has
偽類,也可以輕鬆實現了,如下
.item:has(+.item.current){
transform: translate3d(-30%, 0, -100px);
}
效果如下
不過還有一些臨界情況,比如在第一個卡片時,由於前面沒有兄弟節點了,所以就成了這樣
所以需要再在第一個元素時,把最後一個元素放在它的左邊,第一個元素是:first-child
,最後一個元素是:last-child
,所以實現是這樣的(最後一個元素處理同理)
.item.current:first-child ~ .item:last-child {
transform: translate3d(-30%, 0, -100px);
opacity: 1;
}
.item:first-child:has(~ .item.current:last-child) {
transform: translate3d(30%, 0, -100px);
opacity: 1;
}
這樣就處理好了邊界情況
進一步,還可以向兩側露出兩個卡片,實現也是類似的,完整實現如下
/*當前項*/
.item.current {
transform: translate3d(0, 0, 0);
}
/*當前項右1*/
.item.current + .item,
.item:first-child:has(~ .item.current:last-child) {
transform: translate3d(30%, 0, -100px);
}
/*當前項左1*/
.item:has(+ .item.current),
.item.current:first-child ~ .item:last-child {
transform: translate3d(-30%, 0, -100px);
}
/*當前項右2*/
.item.current + .item + .item,
.item:first-child:has(~ .item.current:nth-last-child(2)),
.item:nth-child(2):has(~ .item.current:last-child) {
transform: translate3d(50%, 0, -150px);
}
/*當前項左2*/
.item:has(+.item + .item.current),
.item.current:first-child ~ .item:nth-last-child(2),
.item.current:nth-child(2) ~ .item:last-child{
transform: translate3d(-50%, 0, -150px);
}
這樣就實現了關於.current
的全部樣式處理了,只用一個變數就控制了所有變化
二、自動輪播和暫停
有了上面的處理,接下來的邏輯就非常簡單了,只需要透過js
動態控制.current
的變化就行了。
一般情況下,我們可能會想到用定時器setInterval
,但這裡,我們也可以不使用定時器,藉助 CSS 動畫的力量,可以更輕鬆地完整這樣的互動。
有興趣的可以參考之前這篇文章:還在用定時器嗎?藉助 CSS 來監聽事件,相信可以給你帶來一些靈感
要做的事情很簡單,給容器新增一個無關緊要的 CSS 動畫
.view {
/**/
animation: scroll 3s infinite;
}
@keyframes scroll {
to {
transform: translateZ(.1px); /*無關緊要的動畫樣式*/
}
}
這樣就得到了一個時長為3s
,無限迴圈的動畫了。
然後,監聽animationiteration
事件,這個事件在動畫每次執行一次就會回撥一次,在這裡也就是每3s
執行一次,就像setInterval
的功能一樣
GlobalEventHandlers.onanimationiteration - Web API 介面參考 | MDN (mozilla.org)
在animationiteration
回撥中處理.current
邏輯,很簡單,移除當前的.current
,給下一個新增.current
,注意一下邊界就行了,具體實現如下
view.addEventListener("animationiteration", () => {
const current = view.querySelector(".current") || view.firstElementChild;
current.classList.remove("current");
if (current.nextElementSibling) {
current.nextElementSibling.classList.add("current");
} else {
view.firstElementChild.classList.add("current");
}
});
使用animationiteration
的最大好處是,可以直接透過 CSS 進行動畫的控制,再也無需監聽滑鼠移入移出事件了
.view:hover{
animation-play-state: paused;
}
效果如下(方便演示,速度調快了)
三、點選快速切換
點選切換,我其實最先想到的是透過:checked
,也就是類似單選,比如
<div class="view" id="view">
<label class="item"><input type="radio" checked name="item"></label>
<label class="item"><input type="radio" name="item"></label>
<label class="item"><input type="radio" name="item"></label>
<label class="item"><input type="radio" name="item"></label>
<label class="item"><input type="radio" name="item"></label>
<label class="item"><input type="radio" name="item"></label>
</div>
但是目前來看:has
偽類貌似不支援多層巢狀,比如下面這條語句
.item:has(+.item:has(:checked)){
/*不生效*/
}
.item:has(:checked)
選中的是子元素被選中的父級,然後.item:has(+.item:has(:checked))
表示選中它的前面一個兄弟節點,這樣就能實現選中功能了,可惜現在並不支援?(以後可能支援)
沒辦法,只能透過傳統方式來實現,直接繫結監聽click
事件
view.addEventListener("click", (ev) => {
const current = view.querySelector(".current") || view.firstElementChild;
current.classList.remove("current");
ev.target.closest('.item').classList.add("current");
});
效果如下
完整程式碼可以檢視線上 demo:CSS 3dscroll(runjs.work)
四、總結一下
以上就是藉助:has
偽類來實現一個3d輪播圖的全部細節了,所有的視覺變化全部在 CSS 中完成,JS 只需要處理切換邏輯就行了,相比以前而言,實現上更加簡潔和優雅,下面總結一下
- 3d 視覺樣式可以透過
transform-style: preserve-3d;
實現近大遠小的效果 - 透過
.item:has(+.item.current)
可以設定當前項前面的兄弟節點 - 還需要考慮第一個和最後一個這兩種臨界情況
- 輪播圖自動輪播和暫停可以藉助
animationiteration
回撥,這種方式的優勢是可以透過:hover
控制 :has
偽類貌似不支援多層巢狀,希望以後可以支援吧~
:has
非常強大,目前唯一的缺陷在於相容性。好在瀏覽器對於這一新特性跟進的都比較積極,明年這個時候差不多可以在內部專案用起來了。最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤
歡迎關注我的公眾號:前端偵探