話說在前頭:文中的行為我都在 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。

這篇算是「不用寫程式」的無障礙功能:瀏覽器默默幫忙,我們只要知道它的存在、別在不知情的情況下把它弄壞就好。

你在瀏覽網頁時,最常被哪種畫面跳動嚇到?歡迎一起討論!

延伸閱讀

相關連結