純CSS實(shí)現(xiàn)電梯導(dǎo)航!
我們經(jīng)常會在博客、文檔中看到類似這樣的側(cè)邊導(dǎo)航目錄,例如:
這種導(dǎo)航也被稱為“電梯導(dǎo)航”(當(dāng)然可能還有其他叫法,知道是這個交互就行)。它會隨著內(nèi)容的滾動而自動切換當(dāng)前選中態(tài),點(diǎn)擊任意目錄也會自動滾動到對應(yīng)標(biāo)題,就像這樣。
通常要實(shí)現(xiàn)這樣一個交互肯定少不了JS,常規(guī)的做法是監(jiān)聽滾動事件,也可以用IntersectionObserver監(jiān)聽元素的滾動位置狀態(tài),下面有一篇關(guān)于用IntersectionObserver的實(shí)現(xiàn)。
嘗試使用JS IntersectionObserver讓標(biāo)題和導(dǎo)航聯(lián)動:https://www.zhangxinxu.com/wordpress/2020/12/js-intersectionobserver-nav 。
大家可能也發(fā)現(xiàn)了,這個交互最大的特點(diǎn)就是滾動,是不是也可以聯(lián)想到 CSS滾動驅(qū)動動畫呢?經(jīng)過一番嘗試,發(fā)現(xiàn)純 CSS也能完美實(shí)現(xiàn),而且實(shí)現(xiàn)更加簡單(不到10行),下面是我復(fù)刻的效果。
是不是非常神奇?CSS 還能實(shí)現(xiàn)這樣的效果?一起看看吧!
一、CSS 滾動錨定
這個導(dǎo)航主要有兩個交互:
- 點(diǎn)擊導(dǎo)航會自動滾動到頁面對應(yīng)位置。
- 頁面滾動會自動切換導(dǎo)航選中態(tài)。
第一條比較容易,我們可以直接用a標(biāo)簽的能力實(shí)現(xiàn)錨定跳轉(zhuǎn)。假設(shè)HTML結(jié)構(gòu)如下:
<nav>
<a>一、標(biāo)題一</a>
<a>二、標(biāo)題二</a>
<a>三、標(biāo)題三</a>
<a>四、標(biāo)題四</a>
<a>五、標(biāo)題五</a>
<a>六、標(biāo)題六</a>
</nav>
<h1>CSS 電梯導(dǎo)航</h1>
<div class="content">
<h2>一、標(biāo)題一</h2>
<section>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</section>
</div>
<div class="content">
<h2>二、標(biāo)題二</h2>
<section>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</section>
</div>
<div class="content">
<h2>三、標(biāo)題三</h2>
<section>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</section>
</div>
<div class="content">
<h2>四、標(biāo)題四</h2>
<section>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</section>
</div>
<div class="content">
<h2>五、標(biāo)題五</h2>
<section>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</section>
</div>
<div class="content">
<h2>六、標(biāo)題六</h2>
<section>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</section>
</div>
然后簡單修飾一下。
body{
padding: 0 15px;
}
h2{
margin: 0;
padding: .8em 0;
scroll-margin: 20px;
}
nav{
position: fixed;
top: 15px;
right: 15px;
background: #fff;
padding: 10px 0;
border-radius: 4px;
overflow: hidden;
}
nav>a{
position: relative;
display: block;
line-height: 2;
padding: 0 15px;
font-size: 14px;
color: #191919;
text-decoration: none;
}
nav>a:hover{
background-color: #d5d5d54a;
}
section{
display: flex;
flex-wrap: wrap;
gap: 10px;
}
section span{
width: 30%;
height: 100px;
border-radius: 4px;
background-color: #E4CCFF;
}
效果如下:
然后我們只需要給a標(biāo)簽添加href屬性,頁面相對應(yīng)的地方指定相同的id,就像這樣。
<nav>
<a href="#t1">一、標(biāo)題一</a>
<a href="#t2">二、標(biāo)題二</a>
...
</nav>
<div class="content">
<h2 id="t1">一、標(biāo)題一</h2>
<section>
...
</section>
</div>
<div class="content">
<h2 id="t2">二、標(biāo)題二</h2>
<section>
...
</section>
</div>
這樣點(diǎn)擊a標(biāo)簽會自動錨點(diǎn)到對應(yīng)位置,效果如下:
這樣就能跳轉(zhuǎn)了,如果你覺得有點(diǎn)生硬,可以加入滾動動畫。
body{
/**/
scroll-behavior: smooth;
}
這樣就平滑多了。
這樣就實(shí)現(xiàn)了滾動錨定效果,還算比較容易。
下面來看如何實(shí)現(xiàn)滾動聯(lián)動效果。
二、CSS 滾動驅(qū)動動畫
我們可以想一下,如果是IntersectionObserver該如何做呢?沒錯,就是監(jiān)聽每一塊區(qū)域的出現(xiàn)時機(jī),然后改變導(dǎo)航的狀態(tài)。
剛好CSS滾動驅(qū)動動畫中的view-timeline可以實(shí)現(xiàn)類似的效果。它可以「監(jiān)測到元素在可視區(qū)」的情況。
不過,單獨(dú)依靠view-timeline還不行,因?yàn)槟J(rèn)情況下,CSS 滾動驅(qū)動作用范圍只能影響到子元素,而我們的dom結(jié)構(gòu)明顯是分離的。
<nav>
<a href="#t1">一、標(biāo)題一</a>
<a href="#t2">二、標(biāo)題二</a>
...
</nav>
<div class="content">
<h2 id="t1">一、標(biāo)題一</h2>
<section>
...
</section>
</div>
<div class="content">
<h2 id="t2">二、標(biāo)題二</h2>
<section>
...
</section>
</div>
為了解決這個問題,我們需要用到 CSS 時間線范圍,也就是 timeline-scope。
https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope
這里簡單介紹一下,假設(shè)有這樣一個結(jié)構(gòu)。
<div class="content">
<div class="box animation"></div>
</div>
<div class="scroller">
<div class="long-element"></div>
</div>
這是兩個元素,右邊的是滾動容器,左邊的是一個可以旋轉(zhuǎn)的矩形。
我們想實(shí)現(xiàn)滾動右邊區(qū)域時,左邊矩形跟著旋轉(zhuǎn),如何實(shí)現(xiàn)呢?
可以給他們共同的父級,比如body定義一個timeline-scope。
body{
timeline-scope: --myScroller;
}
然后,滾動容器的滾動和矩形的動畫就可以通過這個變量關(guān)聯(lián)起來了。
.scroller {
overflow: scroll;
scroll-timeline-name: --myScroller;
background: deeppink;
}
.animation {
animation: rotate-appear;
animation-timeline: --myScroller;
}
效果如下:
這樣就實(shí)現(xiàn)任意元素間的滾動聯(lián)動。
回到這里,我們要做的事情其實(shí)很簡單,給父級(body)定義多個timeline-scope,然后給內(nèi)容區(qū)域和導(dǎo)航區(qū)域都綁定一個相同CSS變量,具體做法如下:
<body style="timeline-scope: --t1,--t2,--t3,--t4,--t5,--t6;">
<nav>
<a href="#t1" style="--s: --t1">一、標(biāo)題一</a>
<a href="#t2" style="--s: --t2;">二、標(biāo)題二</a>
<a href="#t3" style="--s: --t3">三、標(biāo)題三</a>
<a href="#t4" style="--s: --t4">四、標(biāo)題四</a>
<a href="#t5" style="--s: --t5">五、標(biāo)題五</a>
<a href="#t6" style="--s: --t6">六、標(biāo)題六</a>
</nav>
<h1>CSS 電梯導(dǎo)航</h1>
<div class="content" style="--s: --t1">
<h2 id="t1">一、標(biāo)題一</h2>
<section>
...
</section>
</div>
<div class="content" style="--s: --t2">
<h2 id="t2">二、標(biāo)題二</h2>
<section>
...
</section>
</div>
<div class="content" style="--s: --t3">
<h2 id="t3">三、標(biāo)題三</h2>
<section>
...
</section>
</div>
<div class="content" style="--s: --t4">
<h2 id="t4">四、標(biāo)題四</h2>
<section>
...
</section>
</div>
<div class="content" style="--s: --t5">
<h2 id="t5">五、標(biāo)題五</h2>
<section>
...
</section>
</div>
<div class="content" style="--s: --t6">
<h2 id="t6">六、標(biāo)題六</h2>
<section>
...
</section>
</div>
然后給內(nèi)容區(qū)域添加view-timeline-name,導(dǎo)航標(biāo)簽添加 animation-timeline,讓這兩者關(guān)聯(lián)起來,也就是內(nèi)容滾動時,導(dǎo)航的動畫跟著執(zhí)行,這里的動畫很簡單,就是改變導(dǎo)航鏈接的文字顏色和邊框顏色,關(guān)鍵實(shí)現(xiàn)如下:
.content{
view-timeline-name: var(--s);
}
nav>a{
/**/
animation: active;
animation-timeline: var(--s);
}
@keyframes active {
0%,100% {
color: #6f00ff;
border-color: #6f00ff;
}
}
效果如下:
這樣滾動聯(lián)動效果基本就出來了,不過還是有些小問題,接著優(yōu)化。
三、CSS 滾動視區(qū)范圍
前面的實(shí)現(xiàn)其實(shí)還個小問題,右邊的導(dǎo)航會同時選中多個。
很明顯是因?yàn)樽髠?cè)的內(nèi)容同時出現(xiàn)了這兩部分區(qū)域。
如果每一塊內(nèi)容高度更少,那同時選中的就更多了,就像這樣。
而我們需要的肯定是同一時刻只選中一個導(dǎo)航,你可以自己定義規(guī)則,比如后面的優(yōu)先于前面的。
那CSS該如何實(shí)現(xiàn)這樣的效果呢?
其實(shí),這里需要換一種思維,上面的實(shí)現(xiàn)之所以會同時出現(xiàn)多個選中,是因?yàn)橐晠^(qū)范圍太大,是整個屏幕,所以可以同時匹配到多個內(nèi)容區(qū)域。
因此,我們可以手動的減少視區(qū)范圍,一直減少成一條線,這樣無論怎樣滾動,都只會匹配一個區(qū)域。
在這里,我們可以通過view-timeline-inset來手動改變視區(qū)范圍,默認(rèn)是0。
比如我們希望以滾動區(qū)域中間為分割線,只要滾動到達(dá)這個點(diǎn),就高亮當(dāng)前導(dǎo)航,可以這樣實(shí)現(xiàn)。
.content{
view-timeline-name: var(--s);
view-timeline-inset: 50%; /*完整寫法是 50% 50%*/
}
為了方便演示,我在滾動區(qū)域中間加了一條紅色的線,便于觀察。
可以很清楚的發(fā)現(xiàn),只要越過這條線,導(dǎo)航馬上觸發(fā)高亮選中。
當(dāng)然你也可以自己調(diào)整這個臨界線,比如下面的表示在距離滾動區(qū)域底部30%的地方做判斷。
.content{
view-timeline-name: var(--s);
view-timeline-inset: 70% 30%;
}
這樣就實(shí)現(xiàn)了我們想要的效果了,你也可以訪問以下在線鏈接查看實(shí)際效果(chrome 116+)。
- CSS 電梯導(dǎo)航 (codepen.io)[1]
- CSS 電梯導(dǎo)航 (juejin.cn)[2]
四、兼容性和總結(jié)
看似這么多,其實(shí)核心代碼就這幾行。
body{
timeline-scope: --t1,--t2,--t3,--t4,--t5,--t6;
}
.content{
view-timeline-name: var(--s);
view-timeline-inset: 50%;
}
nav>a{
animation: active;
animation-timeline: var(--s);
}
@keyframes active {
0%,100% {
color: #6f00ff;
border-color: #6f00ff;
}
}
包括在HTML中的幾行自定義變量,是不是還不到 10 行?相比 JS實(shí)現(xiàn),代碼更簡單,性能也更好,無需初始化,也不用等待 dom 加載,擴(kuò)展性也強(qiáng)。
唯一的缺點(diǎn)可能是兼容性不足,由于依賴timeline-scope,所以必須Chrome 116+,完整兼容性如下:
下面總結(jié)一下
- 滾動錨定可以借助a標(biāo)簽和#id實(shí)現(xiàn)自動滾動跳轉(zhuǎn)。
- scroll-behavior: smooth可以實(shí)現(xiàn)平滑滾動。
- 默認(rèn)情況下,CSS 滾動驅(qū)動作用范圍只能影響到子元素,但是通過timeline-scope,可以讓任意元素都可以受到滾動驅(qū)動的影響。
- 利用timeline-scope,我們可以將每個內(nèi)容的位置狀態(tài)和每個導(dǎo)航的選中狀態(tài)聯(lián)動起來。
- 右邊的導(dǎo)航會同時選中多個是因?yàn)樽筮叺臐L動視區(qū)太大了,可以同時包含多個內(nèi)容區(qū)域。
- 可以用view-timeline-inset來手動改變視區(qū)范圍,縮小成一條線,這樣無論怎樣滾動,都只會匹配一個區(qū)域
- 兼容性還不足,目前是Chrome 116+。
總的來說,CSS滾動驅(qū)動動畫不愧是2023年度最強(qiáng)特性,可以做的事情太多了,很多 JS才能實(shí)現(xiàn)的交互都可以取代了,而且做的更好,至于兼容性,還是留給時間吧。
[1]CSS 電梯導(dǎo)航 (codepen.io): https://codepen.io/xboxyan/pen/zYVBEWq。
[2]CSS 電梯導(dǎo)航 (juejin.cn): https://code.juejin.cn/pen/7396195867155562508。