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