還在用 Swiper.js 嗎?CSS實(shí)現(xiàn)帶指示器的 Swiper
幾乎每個(gè)前端開(kāi)發(fā)都應(yīng)該用過(guò)這個(gè)滑動(dòng)組件庫(kù)吧?這就是大名鼎鼎的swiper.js
沒(méi)想到已經(jīng)出到 11 個(gè)大版本了 https://www.swiper.com.cn/
當(dāng)然我也不例外,確實(shí)非常全面,也非常強(qiáng)大。
不過(guò)很多時(shí)候,我們可能只用到了它的10%不到的功能,顯然是不劃算的,也會(huì)有性能方面的顧慮。
隨著CSS地不斷發(fā)展,現(xiàn)在純CSS也幾乎能夠?qū)崿F(xiàn)這樣一個(gè)swiper了,實(shí)現(xiàn)更加簡(jiǎn)單,更加輕量,性能也更好,完全足夠日常使用,最近在項(xiàng)目中也碰到了一個(gè)swiper的需求,剛好練一下手,一起看看吧!
一、CSS 滾動(dòng)吸附
swiper有一個(gè)最大的特征就是滾動(dòng)吸附。相信很多同學(xué)已經(jīng)想到了,那就是CSS scroll snap,這里簡(jiǎn)單介紹一下。
看似屬性非常多,其實(shí)CSS scroll snap最核心的概念有兩個(gè),一個(gè)是scroll-snap-type,還一個(gè)是scroll-snap-align,前者是用來(lái)定義吸附的方向和吸附程度的,設(shè)置在「滾動(dòng)容器」上。后者是用來(lái)定義吸附點(diǎn)的對(duì)齊方式的,設(shè)置在「子元素」上。
有了這兩個(gè)屬性,就可以很輕松的實(shí)現(xiàn)滾動(dòng)吸附效果了,下面舉個(gè)例子。
<div class="swiper">
<div class="swiper-item">
<div class="card"></div>
</div>
<div class="swiper-item">
<div class="card"></div>
</div>
<div class="swiper-item">
<div class="card"></div>
</div>
</div>
簡(jiǎn)單修飾一下,讓swiper可以橫向滾動(dòng)。
.swiper {
display: flex;
overflow: auto;
}
.swiper-item {
width: 100%;
display: flex;
justify-content: center;
flex-shrink: 0;
}
.card {
width: 300px;
height: 150px;
border-radius: 12px;
background-color: #9747FF;
}
效果如下:
然后加上scroll-snap-type和scroll-snap-align。
.swiper {
/**/
scroll-snap-type: x mandatory;
}
.swiper-item {
/**/
scroll-snap-align: center;
}
這樣就能實(shí)現(xiàn)滾動(dòng)吸附了。
注意這里還有一個(gè)細(xì)節(jié),如果滑動(dòng)的非???,是可以從第一個(gè)直接滾動(dòng)到最后一個(gè)的,就像這樣。
如果不想跳過(guò),也就是每次滑動(dòng)只會(huì)滾動(dòng)一屏,可以設(shè)置scroll-snap-stop屬性,他可以決定是否“跳過(guò)”吸附點(diǎn),默認(rèn)是normal,可以設(shè)置為always,表示每次滾動(dòng)都會(huì)停止在最近的一個(gè)吸附點(diǎn)。
.swiper-item {
scroll-snap-align: center;
scroll-snap-stop: always;
}
這樣無(wú)論滾動(dòng)有多快,都不會(huì)跳過(guò)任何一屏了。
還有一點(diǎn),現(xiàn)在是有滾動(dòng)條的,顯然是多余的。
這里可以用::-webkit-scrollbar去除滾動(dòng)條。
::-webkit-scrollbar{
width: 0;
height: 0;
}
滑動(dòng)基本上就這樣了,下面來(lái)實(shí)現(xiàn)比較重要的指示器。
二、CSS 滾動(dòng)驅(qū)動(dòng)動(dòng)畫(huà)
首先我們加幾個(gè)圓形的指示器。
<div class="swiper">
<div class="swiper-item">
<div class="card"></div>
</div>
<div class="swiper-item">
<div class="card"></div>
</div>
<div class="swiper-item">
<div class="card"></div>
</div>
<!--指示器-->
<div class="pagination">
<i class="dot"></i>
<i class="dot"></i>
<i class="dot"></i>
</div>
</div>
用絕對(duì)定位定在下方。
.pagination {
position: absolute;
display: inline-flex;
justify-content: center;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
gap: 4px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.36);
transition: 0.3s;
}
效果如下:
那么,如何讓下方的指示器跟隨滾動(dòng)而變化呢?
在這里,我們可以再單獨(dú)繪制一個(gè)高亮的狀態(tài),剛好覆蓋在現(xiàn)在的指示器上,就用偽元素來(lái)代替。
.pagination::before{
content: '';
position: absolute;
width: 6px;
height: 6px;
border-radius: 3px;
background-color: #F24822;
left: 0;
}
效果如下:
然后給這個(gè)高亮狀態(tài)一個(gè)動(dòng)畫(huà),從第一個(gè)指示器位置移動(dòng)到最后一個(gè)。
.pagination::after{
/**/
animation: move 3s linear forwards;
}
@keyframes move {
to {
left: 100%;
transform: translateX(-100%);
}
}
現(xiàn)在這個(gè)紅色的圓會(huì)自動(dòng)從左到右運(yùn)動(dòng),效果如下:
最后,讓這個(gè)動(dòng)畫(huà)和滾動(dòng)關(guān)聯(lián)起來(lái),也就是滾動(dòng)多少,這個(gè)紅色的圓就運(yùn)動(dòng)多少。
.swiper {
/**/
scroll-timeline: --scroller x;
}
.pagination::after{
/**/
animation: move 3s linear forwards;
animation-timeline: --scroller;
}
這樣就基本實(shí)現(xiàn)了指示器的聯(lián)動(dòng)。
當(dāng)然,你還可以換一種動(dòng)畫(huà)形式,比如steps。
.pagination::after{
/**/
animation: move 3s steps(3, jump-none) forwards;
animation-timeline: --scroller;
}
效果如下(可能會(huì)更常見(jiàn))。
你也可以訪問(wèn)以下在線demo
- CSS swiper (juejin.cn)[1]
三、CSS 時(shí)間線范圍
上面的指示器實(shí)現(xiàn)其實(shí)是通過(guò)覆蓋的方式實(shí)現(xiàn)的,這就意味著無(wú)法實(shí)現(xiàn)這種有尺寸變化的效果,例如:
這種情況下,每個(gè)指示器的變化是獨(dú)立的,而且尺寸變化還會(huì)相互擠壓。
那么,有沒(méi)有辦法實(shí)現(xiàn)這樣的效果呢?當(dāng)然也是有的,需要用到 CSS 時(shí)間線范圍,也就是 timeline-scope。
https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope
這是什么意思呢?默認(rèn)情況下,CSS 滾動(dòng)驅(qū)動(dòng)作用范圍只能影響到子元素,但是通過(guò)timeline-scope,可以讓任意元素都可以受到滾動(dòng)驅(qū)動(dòng)的影響。簡(jiǎn)單舉個(gè)例子。
<div class="content">
<div class="box animation"></div>
</div>
<div class="scroller">
<div class="long-element"></div>
</div>
這是兩個(gè)元素,右邊的是滾動(dòng)容器,左邊的是一個(gè)可以旋轉(zhuǎn)的矩形。
我們可以在他們共同的父級(jí),比如body定義一個(gè)timeline-scope。
body{
timeline-scope: --myScroller;
}
然后,滾動(dòng)容器的滾動(dòng)和矩形的動(dòng)畫(huà)就可以通過(guò)這個(gè)變量關(guān)聯(lián)起來(lái)了。
.scroller {
overflow: scroll;
scroll-timeline-name: --myScroller;
background: deeppink;
}
.animation {
animation: rotate-appear;
animation-timeline: --myScroller;
}
效果如下:
我們回到這個(gè)例子中來(lái),很明顯每個(gè)卡片對(duì)應(yīng)一個(gè)指示器,但是他們從結(jié)構(gòu)上又不是包含關(guān)系,所以這里也可以給每個(gè)卡片和指示器一個(gè)相關(guān)聯(lián)的變量,具體實(shí)現(xiàn)如下:
<div class="swiper-container" style="timeline-scope: --t1,--t2,--t3;">
<div class="swiper" style="--t: --t1">
<div class="swiper-item">
<div class="card">1</div>
</div>
<div class="swiper-item" style="--t: --t2">
<div class="card">2</div>
</div>
<div class="swiper-item" style="--t: --t3">
<div class="card">3</div>
</div>
</div>
<div class="pagination">
<i class="dot" style="--t: --t1"></i>
<i class="dot" style="--t: --t2"></i>
<i class="dot" style="--t: --t3"></i>
</div>
</div>
然后,給每個(gè)指示器添加一個(gè)動(dòng)畫(huà)。
@keyframes move {
50% {
width: 12px;
border-radius: 3px 0px;
border-color: rgba(0, 0, 0, 0.12);
background: #fff;
}
}
效果如下:
然后我們需要將這個(gè)動(dòng)畫(huà)和卡片的滾動(dòng)關(guān)聯(lián)起來(lái),由于是需要監(jiān)聽(tīng)卡片的位置狀態(tài),比如只有第二個(gè)出現(xiàn)在視區(qū)范圍內(nèi)時(shí),第二個(gè)指示器才會(huì)變化,所以這里要用到view-timeline,關(guān)鍵實(shí)現(xiàn)如下:
.swiper-item {
/**/
view-timeline: var(--t) x;
}
.dot {
/**/
animation: move 3s;
animation-timeline: var(--t);
}
這樣就實(shí)現(xiàn)了我們想要的效果。
你也可以訪問(wèn)以下在線demo
- CSS swiper timeline scope (juejin.cn)[2]
四、CSS 自動(dòng)播放
由于是頁(yè)面滾動(dòng),CSS 無(wú)法直接控制,所以要換一種方式。通常我們會(huì)借助JS
定時(shí)器實(shí)現(xiàn),但是控制比較麻煩。
沒(méi)錯(cuò),我們這里也可以用這個(gè)原理實(shí)現(xiàn)。
給容器定義一個(gè)無(wú)關(guān)緊要的動(dòng)畫(huà)。
.swiper {
animation: scroll 3s infinite; /*每3s動(dòng)畫(huà),無(wú)限循環(huán)*/
}
@keyframes scroll {
to {
transform: opacity: .99; /*無(wú)關(guān)緊要的樣式*/
}
}
然后監(jiān)聽(tīng)animationiteration事件,這個(gè)事件表示每次動(dòng)畫(huà)循環(huán)就觸發(fā)一次,也就相當(dāng)于每3秒執(zhí)行一次。
swiper.addEventListener("animationiteration", (ev) => {
// 輪播邏輯
if (ev.target.offsetWidth+ev.target.scrollLeft >= ev.target.scrollWidth) {
// 滾動(dòng)到最右邊了直接回到0
ev.target.scrollTo({
left: 0,
behavior: "smooth",
})
} else {
// 每次滾動(dòng)一屏
ev.target.scrollBy({
left: ev.target.offsetWidth,
behavior: "smooth",
});
}
})
相比定時(shí)器的好處就是,可以直接通過(guò)CSS控制播放和暫停,比如我們要實(shí)現(xiàn)當(dāng)鼠標(biāo)放在輪播上是自動(dòng)暫停,可以這樣來(lái)實(shí)現(xiàn),副作用更小
swiper:hover, .swiper:active{
animation-play-state: paused; /*hover暫停*/
}
最終效果如下:
你也可以訪問(wèn)以下在線demo
- CSS swiper autoplay (juejin.cn)[3]
五、回調(diào)事件
swiper很多時(shí)候不僅僅只是滑動(dòng),還需要有一個(gè)回調(diào)事件,以便于其他處理。這里由于是滾動(dòng)實(shí)現(xiàn),所以有必要監(jiān)聽(tīng)scroll事件。
實(shí)現(xiàn)很簡(jiǎn)單,只需要監(jiān)聽(tīng)滾動(dòng)偏移和容器本身的尺寸就可以了,具體實(shí)現(xiàn)如下:
swiper.addEventListener("scroll", (ev) => {
const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth)
console.log(index)
})
效果如下:
你可能覺(jué)得觸發(fā)次數(shù)太多了,我們可以限制一下,只有改變的時(shí)候才觸發(fā)。
swiper.addEventListener("scroll", (ev) => {
const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth)
// 和上次不相同的時(shí)候才打印
if (swiper.index!== index) {
swiper.index = index
console.log(index)
}
})
現(xiàn)在就好一些了。
還可以繼續(xù)優(yōu)化,當(dāng)滑動(dòng)超過(guò)一半時(shí),就認(rèn)為已經(jīng)滑到下一個(gè)卡片了,只需要在原有基礎(chǔ)上加上0.5就行了。
swiper.addEventListener("scroll", (ev) => {
const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth + 0.5)
if (swiper.index!== index) {
swiper.index = index
console.log(index)
}
})
效果如下:
如果在 vue這樣的框架里,就可以直接這樣實(shí)現(xiàn)了。
const current = ref(0)
const scroll = (ev: Event) => {
const swiper = ev.target as HTMLDivElement
if (swiper) {
current.value = Math.floor(swiper.scrollLeft / swiper.offsetWidth + 0.5)
}
}
const emits = defineEmits(['change'])
watch(current, (v) => {
emits('change', v)
})
六、兼容性處理
前面提到的CSS滾動(dòng)驅(qū)動(dòng)動(dòng)畫(huà)兼容性不是很好,需要Chrome 115+,所以對(duì)于不支持的瀏覽器,你也可以用監(jiān)聽(tīng)回調(diào)事件的方式來(lái)實(shí)現(xiàn)指示器聯(lián)動(dòng),就像這樣。
swiper.addEventListener("scroll", (ev) => {
const index = Math.floor(swiper.scrollLeft / swiper.offsetWidth + 0.5)
if (swiper.index!== index) {
swiper.index = index
console.log(index)
if (!CSS.supports("animation-timeline","scroll()")) {
document.querySelector('.dot[data-current="true"]').dataset.current = false
document.querySelectorAll('.dot')[index].dataset.current = true
}
}
})
對(duì)于 CSS部分,還需要用CSS support判斷一下,這樣一來(lái),不支持瀏覽器就不會(huì)自動(dòng)播放動(dòng)畫(huà)了。
@supports (animation-timeline: scroll()) {
.dot{
animation: move 1s;
animation-timeline: var(--t);
}
}
@supports not (animation-timeline: scroll()) {
.dot[data-current="true"]{
width: 12px;
border-radius: 3px 0px;
border-color: rgba(0, 0, 0, 0.12);
background: #fff;
}
}
這樣既使用了最新的瀏覽器特性,又兼顧了不支持的瀏覽器,下面是Safari的效果。
對(duì)比一下支持animation-timeline的瀏覽器(chrome 115+)。
你會(huì)發(fā)現(xiàn),這種效果更加細(xì)膩,指示器是完全跟隨滾動(dòng)進(jìn)度變化的。
也算一種體驗(yàn)增強(qiáng)吧,你也可以訪問(wèn)以下在線demo
- CSS swiper support (juejin.cn)
七、總結(jié)一下
做好兼容,CSS 也是可以嘗試最新特性的,下面總結(jié)一下要點(diǎn)
- swiper 非常強(qiáng)大,我們平時(shí)可能只用到了它的10%不到的功能,非常不劃算。
- CSS發(fā)展非常迅速,完全可以借助 CSS代替部分swiper。
- 滾動(dòng)吸附比較容易,需要借助CSS scroll snap完成。
- 指示器聯(lián)動(dòng)可以用CSS滾動(dòng)驅(qū)動(dòng)動(dòng)畫(huà)實(shí)現(xiàn),讓指示器唯一動(dòng)畫(huà)和滾動(dòng)關(guān)聯(lián)起來(lái),也就是滾動(dòng)多少,指示器就偏移多少。
- 默認(rèn)情況下,CSS 滾動(dòng)驅(qū)動(dòng)作用范圍只能影響到子元素,但是通過(guò)timeline-scope,可以讓任意元素都可以受到滾動(dòng)驅(qū)動(dòng)的影響。
- 利用timeline-scope,我們可以將每個(gè)卡片的位置狀態(tài)和每個(gè)指示器的動(dòng)畫(huà)狀態(tài)聯(lián)動(dòng)起來(lái)。
- 自動(dòng)播放可以借助animationiteration回調(diào)事件,相比JS定時(shí)器,控制更加方便,副作用更小。
- 回調(diào)事件需要監(jiān)聽(tīng)scroll實(shí)現(xiàn),只需要監(jiān)聽(tīng)滾動(dòng)偏移和容器本身的尺寸的比值就行了。
- 對(duì)于不兼容的瀏覽器,也可以通過(guò)回調(diào)事件手動(dòng)關(guān)聯(lián)指示器的狀態(tài)。
- 兼容性判斷,JS可以使用CSS.supports,CSS可以使用@supports。
當(dāng)然,swiper的功能遠(yuǎn)不止上面這些,但是我們平時(shí)遇到的需求可能只是其中的一小部分,大可以通過(guò)CSS方式去實(shí)現(xiàn),充分發(fā)揮瀏覽器的特性,量身定制才會(huì)有足夠的性能和體驗(yàn)。
[1]CSS swiper (juejin.cn): https://code.juejin.cn/pen/7391010495207047205
[2]CSS swiper timeline scope (juejin.cn): https://code.juejin.cn/pen/7391018122460954636
[3]CSS swiper autoplay (juejin.cn): https://code.juejin.cn/pen/7391025055079890995