CSS Scroll Snap 怎麼避開固定導覽列:scroll-padding 與 scroll-margin
CSS Scroll Snap 很適合橫向卡片列、逐頁閱讀與章節導覽;實作後卻常出現一個更惱人的問題:捲動確實吸附了,但目標標題被固定導覽列蓋住。
關鍵是分清楚設定位置:scroll-snap-type 和 scroll-padding 放捲動容器;scroll-snap-align 和 scroll-margin 放每一個吸附目標。 固定高度先用 CSS 描述;header 若會因換行、字型或視窗寬度改變高度,仍需同步更新偏移值。只想讓錨點避開導覽列時,不必另外啟用 Scroll Snap。
先建立真的能捲動的容器
<div class="story-list"> <article class="story-card">第一段</article> <article class="story-card">第二段</article> <article class="story-card">第三段</article></div>.story-list { display: grid; grid-auto-flow: column; grid-auto-columns: 85%; gap: 1rem; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 1rem;}
.story-card { scroll-snap-align: start;}Scroll Snap 的前提是容器有可捲動範圍。mandatory 會要求結束位置對齊 snap point;若讀者需要停在兩個項目間讀較長內容,可考慮 proximity,讓瀏覽器只在接近目標時吸附。
固定 header 用 scroll-padding-top
頁面本身是捲動容器時,可把已知的 header 高度變成 scrollport 的內距:
:root { --header-height: 4.5rem;}
html { scroll-snap-type: y proximity; scroll-padding-top: var(--header-height);}
article h2 { scroll-snap-align: start;}這個設定也會影響 scrollIntoView() 與 fragment navigation 的可視對齊;它描述的是「容器的可用可視區域」,所以放在 html 或真正有 overflow 的面板,而不是隨意放到每個標題。
若容器已預留 header 高度,只有某個錨點還需要額外空間,才在目標用 scroll-margin-top:
#pricing { scroll-margin-top: 1rem;}兩個屬性分別調整容器的可視對齊區域與目標的對齊範圍;它們可以一起生效,並不是後者覆蓋前者。以上例的 start 對齊而言,header 高度由容器負責,目標只再加 1rem。不要在兩邊都填入完整 header 高度,否則會留下過大的間距。靠近文件底部時,可捲動距離也可能不足,不能把所有落點都當成固定數字保證。
另一種選擇是容器不設 header 偏移,統一在 heading 上設 scroll-margin-top: calc(var(--header-height) + 1rem)。兩種策略擇一作為共用規則,再讓個別元件加必要的差額。
依 CSS Scroll Snap 規格,scroll-padding 與 scroll-margin 也用於未啟用 snapping 的捲動定位。一般文章目錄可以只留下偏移設定,移除 scroll-snap-type 與 heading 的 scroll-snap-align,避免讀者每次捲動都被吸附。這些屬性不會增加一般排版的 padding 或 margin,也不會把標題從 header 下方推開;它們作用在捲動定位。
落點仍不對,先找誰真的在捲
在水平書寫的頁面,依序檢查以下三件事:
- 用開發者工具確認捲動的是文件還是內層面板。文件可先檢查
document.scrollingElement;面板則看自己的scrollHeight是否大於clientHeight,以及捲動時scrollTop是否改變。 - 檢查該容器的 computed
scroll-padding-top,以及目標的 computedscroll-margin-top。值為0px或被其他樣式覆寫時,先修正 selector 與 cascade。 - 固定使用
block: 'start'測試落點,再比較帶 fragment 的直接載入、目錄點擊與鍵盤 focus。nearest是盡量少捲動,已可見的目標不一定會重新對齊到頂端。
可在自己的頁面 DevTools Console 執行以下診斷示意。先把 selector 改成真正的標題;若是內層面板,把 scroller 改成該面板元素:
const target = document.querySelector('#pricing');const scroller = document.scrollingElement;if (!target || !scroller) throw new Error('請先確認目標與捲動容器');console.table({ overflowY: getComputedStyle(scroller).overflowY, scrollPaddingTop: getComputedStyle(scroller).scrollPaddingTop, scrollMarginTop: getComputedStyle(target).scrollMarginTop, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight,});target.scrollIntoView({ behavior: 'instant', block: 'start' });這段用來讀取樣式與嘗試定位,不會自動辨認所有巢狀捲動容器。本次維護流程於 2026-10-07 在 macOS 的 Chrome 測試頁點擊 fragment 連結:容器設定 scroll-padding-top: 72px,目標的 margin 分別為 0px、16px、88px,量到的頂端落差依序是 72px、88px、160px。測試頁保留足夠的前後捲動空間,沒有啟用 snapping;這確認了該環境的偏移累加,沒有涵蓋 Safari、觸控、動態 header 或文件底部的行為。前面的 Console 診斷片段尚未在產品頁執行。
實際驗收時,應在 header 換行、文字放大與內容高度改變後再看標題是否可讀;單看 computed 值正確不能證明畫面沒有遮擋。若 header 高度會變,可用 ResizeObserver 的尺寸量測與生命週期做法 更新 CSS 變數,但不要在 callback 中反覆改動被觀察元素的尺寸。
輪播別只靠強制吸附
Scroll Snap 解決的是捲動落點,不會替你補上輪播的可存取操作。若卡片是可互動內容,至少要讓鍵盤能移動到卡片內的連結或按鈕;不要把資訊只放在滑動才能看到的區塊。也要用實機檢查觸控板、鍵盤與 reduced motion 偏好下的感受。
如果問題是「呼叫 scrollIntoView() 後沒有捲到正確元素」,先確認你拿到的是單一 DOM element,而不是先把 CSS 吸附規則當成錯誤來源;可搭配 scrollIntoView 常見錯誤與定位方式 排查。
參考資料: