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