話說在前頭:文中的行為我都在 Chromium 上實測過,其他瀏覽器的細節可能不同,有講錯的地方歡迎指正。
前言
你一定遇過:文章讀到一半,上方的圖片或廣告晚了幾秒才載入,畫面突然往下一跳,剛剛讀到哪裡都找不到了。如果手指剛好要點連結,還可能點到別的東西。
這種畫面跳動對每個人都很煩,但對某些使用者影響特別大:
- 認知障礙或注意力容易分散的人:好不容易專心讀到一個段落,一跳就斷掉了。
- 動作控制比較困難的人:瞄準一個按鈕要花比較多時間,按下去之前畫面一動,就點錯了。
- 把畫面放大閱讀的人:畫面上只看得到一小塊,跳一下就完全迷失位置。
瀏覽器其實內建了一個機制處理這件事,叫做捲動錨定(Scroll Anchoring),對應的 CSS 屬性是 overflow-anchor。它是 CSS 與 HTML 技巧清單裡少數「預設就開著、很多人卻不知道」的功能,Safari 補上之後,三大瀏覽器都到齊了。
先看一下你的瀏覽器支援度:
你的瀏覽器支援度
overflow-anchor偵測中(需要 JavaScript)
一、它在做什麼?
概念很簡單:瀏覽器會在畫面上挑一個元素當作「錨點」。上方的內容變高或變矮時,瀏覽器自動調整捲動位置,讓這個錨點留在畫面上的原位。
這個功能預設就是開啟的,你什麼都不用寫。需要關掉的時候才寫:
.chat-log {
overflow-anchor: none;
}
二、動手試試
下面的方框是一篇文章,紫色那段是「你正在讀的段落」。先按「捲到第 5 段」,再按「模擬慢慢載入」,1.5 秒後最上面會多出一塊晚到的內容:
互動範例:先「捲到第 5 段」,再按「模擬慢慢載入」,看紫色那段會不會被推走
第 1 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 2 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 3 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 4 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 5 段:你正在讀這一段。
第 6 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 7 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 8 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 9 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 10 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 11 段:這是一段示範文字,假裝你正在讀一篇長文章。
第 12 段:這是一段示範文字,假裝你正在讀一篇長文章。
還沒載入。
此處為互動範例,請在網頁上操作。
建議照這幾種組合玩玩看:
| 情況 | 第 5 段會怎樣 |
|---|---|
| 捲到第 5 段,什麼都不勾 | 留在原位,捲動錨定生效 |
捲到第 5 段,勾「overflow-anchor: none」 | 被往下推 |
| 停在最上面,什麼都不勾 | 被往下推 |
| 停在最上面,勾「預留空間」 | 留在原位 |
第三種情況最值得注意:捲動位置在最上面的時候,捲動錨定不會作用。我實測在捲動位置是 0 的時候插入內容,下面的段落就是會被往下推。
三、實測:什麼時候有效、什麼時候幫不上忙
我另外用比較單純的頁面測了幾種情況:
| 情況 | 結果 |
|---|---|
| 已經往下捲,上方插入新內容 | 你正在看的位置不動 |
| 已經往下捲,上方一張沒設尺寸的圖片載入完成 | 不動 |
| 已經往下捲,上方的內容被移除 | 不動 |
| 已經往下捲,上方完全看不到的元素變高 | 不動 |
| 捲動位置在最上面,上方插入內容 | 被推走 |
捲動區塊設了 overflow-anchor: none | 被推走 |
| 畫面上正在看的那個元素自己變高 | 它下面的內容會被推走 |
最後一種的原因是:錨點通常就是你正在看的那個元素,它自己變高時,瀏覽器保住的是它的頂端,它下面的東西還是會被推下去。
另外,overflow-anchor: none 不一定要設在整個捲動區塊上。我實測只設在「會晚到的那一塊」上,瀏覽器就不會選它當錨點,其他段落照樣受保護。
四、什麼時候要關掉它?
大部分情況下,維持預設就好。需要關掉的通常是你已經自己用 JavaScript 控制捲動位置的地方,免得兩邊打架。例如:
- 聊天室、即時紀錄:新訊息加在最下面,你的程式會在使用者停在底部時自動捲到最新一則。這時讓瀏覽器再補一次位置,反而會出問題。
- 自己實作的無限捲動或虛擬清單:程式已經精準計算捲動位置了。
五、它只是保險,不是解法
捲動錨定能減少跳動,但前面也看到了,它有幫不上忙的時候。最根本的做法,還是從源頭預留空間,也就是範例裡「預留空間」那個選項:
<!-- 圖片寫上尺寸,載入前就先佔好位置 -->
<img src="photo.jpg" width="800" height="450" alt="…" />
/* 或是用 aspect-ratio、min-height 幫晚到的區塊留位置 */
.ad-slot {
min-height: 250px;
}
預留了空間,連「停在最上面」這種捲動錨定幫不上忙的情況也不會跳。這也跟 Core Web Vitals 裡的 CLS(累積版面位移)有關。CLS 量的是使用者在畫面上看到的位移:我實測捲動錨定生效時,畫面上的內容沒有移動,CLS 是 0;但停在最上面、或關掉捲動錨定時,同樣插入 200px 的區塊,CLS 就是 0.25。預留空間才能讓兩種情況都不跳。
六、套用到我的網站
寫這篇時,我順便檢查了自己的網站,找到兩個實際的例子:一個是捲動錨定已經在默默幫忙,另一個是它幫不上忙、要從源頭修。
例子一:部落格列表的分類標籤
剛做好的部落格列表,每篇文章下面都有一個可以點的分類標籤,點了會篩選出同分類的文章。我實測往下捲到第 10 篇,點它的「設計與 AI」標籤:
- 預設(捲動錨定開著):篩選後列表只剩 5 篇。瀏覽器把我點的那篇當成錨點,讓它留在原本的位置;它剛好是結果的第一篇,所以畫面上直接看到「共 5 篇文章」和結果開頭。
- 我把列表設成
overflow-anchor: none對照:頁面一下子變短,畫面停在一片空白的中段,結果開頭跑到視窗上方很遠的地方,要自己往上捲才找得到。
這裡不需要加任何程式,預設行為就是對的。不過它也有限制:如果點的那篇在結果裡不是第一篇,前面的結果就會在畫面上方。捲動錨定只負責「你剛剛看的東西不要跑掉」,不負責「讓你看到最重要的內容」。
例子二:產品頁的圖片沒寫尺寸
我用 PerformanceObserver 量網站各頁載入時的版面位移,並故意讓圖片晚 1.5 秒才載入,模擬網路慢的情況。結果產品頁的版面位移分數(CLS)是 0.1263,超過 Google 建議的 0.1。原因是產品頁的圖片都沒有寫 width、height,圖片載入前高度是 0,載入後才把下面的內容推開。
這剛好可以拿來驗證捲動錨定的兩面:
- 如果已經往下捲,正在讀中段的文字,上方的圖片才載入:捲動錨定讓我正在看的標題留在原位(實測只差不到 1px)。
- 如果還在頁面最上面,也就是剛打開頁面的時候:捲動錨定不作用,內容整個被往下推。
所以這裡要從源頭修。我寫了一個小腳本,讀出網站所有圖片的原始寬高存成一份清單,產品頁的 <img> 就依清單補上 width、height:
<img src="/images/accesserty-thumbnail.webp" width="1280" height="685" alt="…" />
搭配全站原本就有的 img { max-width: 100%; height: auto; },瀏覽器會依寬高比例先把空間留好,畫面上的大小完全不變。修正後用同樣的條件再量一次,CLS 從 0.1263 降到 0。
為了避免之後新增圖片時忘記,我也在 build 的資料檢查裡加了一條:產品頁用到的圖片如果不在尺寸清單裡,build 就會失敗,提醒我重新產生清單。
七、順手修掉的:平滑捲動
檢查的過程中,我發現網站對整個頁面設了平滑捲動:
html {
scroll-behavior: smooth;
}
但沒有判斷使用者是否開了「減少動態效果」。對動態敏感的使用者來說,點一下連結畫面就滑一大段,可能會頭暈。WCAG 2.3.3「互動產生的動畫」就是在講這件事。修正方式是只在使用者沒有要求減少動態效果時才開啟:
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
網站右下角「回頁面頂端」按鈕用的 window.scrollTo({ behavior: "smooth" }) 也一樣,CSS 管不到 JavaScript,要另外判斷:
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
結論
整理成幾句話:
- 捲動錨定預設就開著:上方內容變動時,讓你正在看的位置留在原地。
- 捲動位置在最上面時它不會作用,錨點自己變高時也擋不住。
- 自己用 JavaScript 控制捲動的地方,用
overflow-anchor: none關掉,也可以只關掉會晚到的那一塊。 - 它只是保險,從源頭預留空間才是根本解法。
- 我的網站:部落格列表的分類標籤已經在受惠;產品頁的圖片補上寬高後,CLS 從 0.1263 降到 0。
- 順便檢查一下網站的
scroll-behavior: smooth有沒有搭配prefers-reduced-motion。
這篇算是「不用寫程式」的無障礙功能:瀏覽器默默幫忙,我們只要知道它的存在、別在不知情的情況下把它弄壞就好。
你在瀏覽網頁時,最常被哪種畫面跳動嚇到?歡迎一起討論!
延伸閱讀
- 只用 CSS 做輪播,無障礙夠嗎?拿我的 UI Kit 來對照
- 中英文之間要不要打空白?text-autospace 讓瀏覽器自動補間距
- 現代 CSS 與 HTML 技巧整理清單:115 個特性、支援度與實驗優先序