CSS Scroll Marker 怎麼做?用 ::scroll-marker 建立可存取輪播導覽
只有一排可以左右捲動的卡片時,讀者未必知道還有幾張、目前在哪一張,鍵盤使用者也可能找不到下一個內容。CSS Overflow 5 提供的 ::scroll-marker、::scroll-marker-group 與 ::scroll-button(),可以把這些導覽控制交給瀏覽器產生。
先說限制:這組 API 目前仍被 MDN 標為 experimental、不是 Baseline,瀏覽器支援不能當成現成的 production 基線。適合把它當 progressive enhancement:不支援時保留一般的水平捲動或明確的 HTML 連結,支援時再取得原生 marker、按鈕與狀態管理。
直接答案是:先讓元素成為真正的 scroll container,再設定 scroll-marker-group: after,最後在每個代表內容的子元素上設定非 none 的 ::scroll-marker { content: ... }。 沒有 marker group,或沒有 content,marker 就不會出現。
四個 CSS 元件各自負責什麼
| API | 放在哪裡 | 負責什麼 |
|---|---|---|
scroll-marker-group | scroll container 的 property | 決定是否產生 marker group,以及 group 放在內容前或後;最新語法還能選 links 或 tabs 模式 |
::scroll-marker-group | scroll container 的 pseudo-element | 排版與裝飾瀏覽器產生的 marker 容器 |
::scroll-marker | 每個輪播項目的 pseudo-element | 產生指向該項目的導覽 link |
::scroll-button(left/right) | scroll container 的 pseudo-element | 產生前後捲動按鈕;瀏覽器負責按鈕狀態與捲動行為 |
::scroll-marker 不是普通的圓點背景。瀏覽器會把它當成指向來源元素的 anchor,並提供 :target-current 狀態;在支援的實作中,鍵盤與輔助技術也會取得相應的語意。這仍然需要你用真實內容測試,不能只看畫面上有沒有圓點。
先建立一個真正可捲動的輪播
HTML 不需要為每一個 marker 另外建立按鈕。保留正常的 list 結構,並為項目提供清楚的內容:
<ul class="carousel" aria-label="產品特色"> <li> <h2>快速搜尋</h2> <p>用關鍵字找到需要的功能。</p> </li> <li> <h2>離線可讀</h2> <p>沒有網路時仍保留已載入內容。</p> </li> <li> <h2>鍵盤友善</h2> <p>不使用滑鼠也能完成導覽。</p> </li></ul>先讓沒有 marker 的版本也能工作:
.carousel { display: flex; gap: 1rem; margin: 0; padding: 0 1rem 1rem; overflow-x: auto; overscroll-behavior-inline: contain; scroll-snap-type: inline mandatory; scroll-behavior: smooth;}
.carousel > li { flex: 0 0 min(80vw, 24rem); min-block-size: 12rem; padding: 1.25rem; list-style: none; scroll-snap-align: start; border: 1px solid #cbd5e1; border-radius: 0.75rem;}
@media (prefers-reduced-motion: reduce) { .carousel { scroll-behavior: auto; }}這裡先使用 logical properties,讓輪播不被寫死在水平 writing mode;如果你的元件只支援由左至右的水平卡片,也可以改成 overflow-x、scroll-snap-type: x mandatory 與 scroll-snap-align: start。
用 scroll-marker-group 產生 marker
接著在 scroll container opt in,並從每個 li 產生 marker:
.carousel { scroll-marker-group: after; counter-reset: carousel-card;}
.carousel > li { counter-increment: carousel-card;}
.carousel::scroll-marker-group { display: flex; justify-content: center; gap: 0.5rem; padding-block: 0.75rem;}
.carousel > li::scroll-marker { content: counter(carousel-card); inline-size: 0.75rem; block-size: 0.75rem; border: 1px solid currentColor; border-radius: 50%; color: #475569;}
.carousel > li::scroll-marker:focus-visible { outline: 2px solid currentColor; outline-offset: 3px;}
.carousel > li::scroll-marker:target-current { background: currentColor;}最容易漏掉的是 scroll-marker-group: after。這個 property 的初始值是 none;只有 group 被產生後,來源元素的 ::scroll-marker 才有容器可以放入。另一個必要條件是 content 不能是 none,否則 marker 不會生成。
after 代表 marker group 會出現在內容之後,也會排在相應的 tab order 後面。若你把 group 用 CSS 視覺上移到內容前方,應改用 before,讓視覺順序與鍵盤順序一致。
links 與 tabs 怎麼選
目前的基本寫法 scroll-marker-group: after 是 navigation/links 方向。Chrome 154 beta 的說明也列出新的模式關鍵字:
/* 一般輪播、章節導覽:marker 是 links */.carousel { scroll-marker-group: after links;}
/* 每次只把一個 panel 當成目前頁面:marker 是 tabs */.feature-tabs { scroll-marker-group: after tabs;}兩者不是外觀名稱,而是不同的互動與輔助技術語意:
| 模式 | 語意與鍵盤行為 | 適合情況 |
|---|---|---|
links | group 接近 navigation,marker 是 links;每個 marker 可成為 sequential tab stop,啟動後焦點移到目標 | 產品卡片、文章章節、圖片導覽,所有內容都仍屬於同一份可捲動文件 |
tabs | group 接近 tablist,marker 是 tabs;目前 marker 保留 tab stop,其他 marker 用方向鍵切換,非目前 panel 會從 accessibility tree 隱藏 | 內容確實是互斥的 tab panel,讀者一次只應閱讀目前頁面 |
如果只是想做幾個進度圓點,不要為了外觀使用 tabs。把原本同時存在的文章或商品卡片標成互斥 panel,可能讓螢幕閱讀器使用者失去尚未選取的內容;這是語意選擇,不是 CSS 樣式切換。
兩個關鍵字的支援仍可能落後於規格,應把它視為增強層。若瀏覽器只接受單一的 after,先使用 links 方向的基本版本,再由 feature detection 決定是否啟用 tabs。
加上 ::scroll-button() 前後按鈕
marker 適合跳到指定項目,前後按鈕則適合觸控板、滑鼠或不想逐一點 marker 的讀者:
.carousel::scroll-button(left) { content: "←" / "往前捲動";}
.carousel::scroll-button(right) { content: "→" / "往後捲動";}
.carousel::scroll-button(*) { inline-size: 2.25rem; block-size: 2.25rem; border: 1px solid #cbd5e1; border-radius: 50%; background: Canvas;}
.carousel::scroll-button(*):disabled { opacity: 0.4;}
.carousel::scroll-button(*):focus-visible { outline: 2px solid currentColor; outline-offset: 3px;}Chrome 的 CSS carousel 說明指出,scroll button 預設大約移動 scroll area 的 85%;它不保證剛好只前進一張卡片。如果需求是每次只移動一個 snap item,可以在每個項目加上 scroll-snap-stop: always,再用實機測試快速連按、觸控拖曳和鍵盤操作的結果。
content 後面的 / "往前捲動" 是 accessible fallback text,不是畫面上一定要顯示的第二段文字。即使瀏覽器替你建立了按鈕與 link,也要保留清楚的焦點樣式與可理解的 label。
沒有 marker 時的 fallback
這組 API 尚未成為跨瀏覽器基線,所以基礎版面必須在沒有 generated UI 時仍可用。需要更穩定控制語意或支援縮圖導覽時,直接提供 HTML anchor:
<nav class="carousel-fallback" aria-label="產品特色導覽"> <a href="#feature-search">快速搜尋</a> <a href="#feature-offline">離線可讀</a> <a href="#feature-keyboard">鍵盤友善</a></nav>對應的 li 加上 id:
<li id="feature-search">...</li><li id="feature-offline">...</li><li id="feature-keyboard">...</li>如果希望支援 marker 的瀏覽器不要看到兩份導覽,可以用 feature detection 隱藏 fallback:
@supports selector(.carousel::scroll-marker) and (scroll-marker-group: after) { .carousel-fallback { display: none; }}不支援 selector() 或 ::scroll-marker 的瀏覽器會保留 fallback;這正是 progressive enhancement 的目標。若你需要從既有 <a> 元素建立 marker group,也可以研究 scroll-target-group,但它和瀏覽器生成的 ::scroll-marker 在語意與控制權上不同,不要未測試就混用兩套導覽。
用 @supports 把實驗性 CSS 隔離
實際專案可以把增強層集中,讓基本 scroll container 更容易維護:
@supports selector(.carousel::scroll-marker) and (scroll-marker-group: after) { .carousel { scroll-marker-group: after; }
.carousel::scroll-button(left), .carousel::scroll-button(right) { content: ""; }
.carousel > li::scroll-marker { content: ""; }}不要只以 CSS.supports("scroll-marker-group", "after") 判斷全部功能已可用;property、pseudo-element、button 以及 links/tabs 模式可能分開實作。需要啟用進階模式時,分別測試對應 selector 和宣告,並保留 HTML fallback。
上線前要測什麼
瀏覽器生成控制項可以減少手寫狀態,但不能代替元件 QA。至少檢查:
- marker group 的視覺位置與
before/after的鍵盤順序一致。 - 用 Tab、Shift+Tab、方向鍵與 Enter 操作,焦點不會消失在 overflow 區域裡。
- 讀屏軟體讀到的 link/tab label 能區分每個項目;不要只剩沒有意義的圓點。
:target-current在手指拖曳、scroll button、marker 啟動後都能切換。- 觸控、滑鼠滾輪、觸控板與快速連按時沒有卡在兩個 snap point 之間。
prefers-reduced-motion: reduce下不強迫 smooth scroll。- 不支援 API 的瀏覽器仍能讀到全部內容,並使用 HTML fallback 完成導覽。
如果輪播同時有固定 header,還要替 snap target 預留上方空間,可參考 CSS Scroll Snap 避開固定導覽列的設定。若你改用 JavaScript 自行跳到卡片,則要另外確認 scroll container,避免和 scrollIntoView 滾錯容器的排查方式 混成同一個問題。
常見問題
Q: 為什麼 ::scroll-marker 完全沒有出現?
A: 先確認來源元素的祖先是 scroll container,該容器有 scroll-marker-group: before 或 after,以及 marker 的 content 不是 none。如果條件都符合,仍要確認目前瀏覽器是否實作這項 experimental API。
Q: scroll-marker-group: after 和 after links 有什麼差別?
A: after 是基本的 marker group 位置寫法,行為接近 links;after links 是明確指定 links 模式的最新語法。瀏覽器支援不一致時,先以 after 作為較保守的增強層,再對 links/tabs 做 feature detection。
Q: 有了 scroll marker 還需要 JavaScript 嗎?
A: 基本的跳轉、目前狀態、前後捲動與鍵盤行為可以由支援的瀏覽器處理,因此不一定需要 JavaScript。但如果你需要循環輪播、遠端資料、複雜的縮圖、跨瀏覽器一致行為或分析事件,仍可能需要 JavaScript;請保留無 JavaScript 時可讀、可捲動的基礎版面。
Q: tabs 模式可以套在所有卡片輪播嗎?
A: 不建議。tabs 對非目前 panel 的內容有更嚴格的 accessibility 行為,只有當內容真的互斥、讀者一次只應看到一個 panel 時才適合。一般產品卡片或文章導覽通常先使用 links 方向。
參考資料:
Chrome for Developers:Carousels with CSS
Chrome for Developers:Chrome 154 beta
MDN:
::scroll-markerpseudo-element
回報錯字、失效連結,或告訴我你想看的延伸主題。