滑動以查看

滑動顯示模式可讓使用者水平滑動前景列或內容窗格,顯示隱藏的 UI,例如關聯動作按鈕 (「釘選」、「靜音」、「封存」、「刪除」) 或次要側邊選單,且不會自動啟用任何動作。容器不會在滑動時執行動作,而是會彈開,顯示使用者可檢查及啟動的互動控制項,或向後滑動關閉。

如要使用全寬滑動手勢,在超過門檻時立即提交並關閉項目,請參閱「滑動移除」。如要顯示垂直捲動容器頂端的隱藏標頭,請參閱「下拉顯示」。

導入方式

無論是要顯示清單項目上的動作按鈕,還是可捲動窗格旁的側邊選單,機制都相同:將前景內容和隱藏面板包裝在外部水平捲動容器中,並使用 CSS 捲動貼齊 (scroll-snap-type: x mandatory) 和 overscroll-behavior: chain 設定。瀏覽器的原生捲動引擎會處理動量、貼齊、鍵盤焦點捲動和捲動鏈結,不需使用 JavaScript 手勢事件監聽器。

步驟 1:標記滑動包裝函式、內容和顯示面板

在 .SwipeReveal 捲動容器中,先放置前景內容,再放置顯示的控制項 (動作按鈕或側邊選單)。由於內容在 DOM 順序中排在第一位,捲動容器自然會從捲動偏移 0 開始,面板則隱藏在畫面右側。

<!-- Example A: List item with revealable action buttons -->
<div class="SwipeReveal">
  <div class="SwipeReveal-content">
    <strong>Design review notes</strong>
    <p>Updated component specs for the Q3 release.</p>
  </div>
  <div class="SwipeReveal-panel" role="group" aria-label="Item actions">
    <button type="button">Pin</button>
    <button type="button">Archive</button>
    <button type="button">Delete</button>
  </div>
</div>

<!-- Example B: Scrollable content pane with a revealable side menu -->
<div class="SwipeReveal" role="region" aria-label="Document preview with swipeable side menu">
  <div class="SwipeReveal-content is-scrollable" tabindex="0">
    <h2>Article Preview</h2>
    <p>Scroll vertically through this pane, or swipe left to reveal the side menu...</p>
  </div>
  <nav class="SwipeReveal-panel" aria-label="Contextual menu">
    <a href="#share">Share</a>
    <a href="#bookmark">Bookmark</a>
    <a href="#export">Export PDF</a>
  </nav>
</div>

步驟 2:設定水平貼齊容器和 overscroll-behavior: chain

如果將元素包裝在外部捲動容器中,只是為了建立滑動顯示效果,該外部包裝函式就會成為文件 (或其他父項捲動容器) 內的中介捲動器。

overscroll-behavior 的四個關鍵字可獨立控制多餘的捲動是否會傳播到祖系捲動容器 (捲動鏈結),以及容器是否會顯示本機過度捲動功能,例如彈性橡皮筋拉伸或邊緣發光 (本機邊界效果):

值 捲動鏈結 (傳播至祖系) 本機邊界效果 (橡皮筋/發光) 滑動揭示廣告中的角色
auto 是 是 預設行為;在需要正常彈跳回饋的內部內容捲動器上,請保持開啟此功能,但請避免在外部滑動包裝器上開啟,以免選單彈跳
chain 是 否 在外部 .SwipeReveal 捲動器上使用,讓多餘的捲動鏈結到父項捲動器 (例如文件),而不會彈出選單或動作條
contain 否 是 在容器內捲動時會觸發陷阱,但仍會在本機彈跳
none 否 否 同時封鎖本機彈跳和捲動鏈結至上層元素

將 .SwipeReveal 設為雙欄格線 (grid-template-columns: 100% max-content),其中 .SwipeReveal-content 會貼齊 start,.SwipeReveal-panel 則會貼齊 end:

.SwipeReveal {
  /* Column 1: full-width content. Column 2: auto-sized actions or side menu. */
  display: grid;
  grid-template-columns: 100% max-content;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Prevent the swipe wrapper and revealed panel from bouncing locally while
     still chaining excess scroll into the parent scroller (the document). */
  overscroll-behavior: chain;
}

@media (prefers-reduced-motion: no-preference) {
  .SwipeReveal {
    scroll-behavior: smooth;
  }
}

.SwipeReveal-content {
  scroll-snap-align: start;
  scroll-initial-target: nearest;
  background: Canvas;
}

.SwipeReveal-content:focus-visible {
  outline: auto;
  outline-offset: -2px;
}

.SwipeReveal-content.is-scrollable {
  block-size: 20rem;
  overflow: auto;
  /* Keeps default `overscroll-behavior: auto` so the content pane retains
     its normal bounce effect while chaining horizontal swipes to `.SwipeReveal`. */
}

.SwipeReveal-panel {
  /* Snapping a trailing panel to `end` (or a leading panel to `start`) brings
     all controls fully into view while keeping the adjacent edge of the content visible. */
  scroll-snap-align: end;
  display: flex;

  &:first-child {
    scroll-snap-align: start;
  }
}

如果您也想使用三欄 (grid-template-columns: max-content 100% max-content) 或兩欄 (max-content 100%) 顯示左側 (inline-start) 的控制項,請將開頭的 .SwipeReveal-panel 放在 .SwipeReveal-content 之前。由於 .SwipeReveal-content 具有 scroll-initial-target: nearest,捲動容器一開始會停留在內容欄,而非前置面板。

步驟 3:在鍵盤焦點上,將可聚焦的內容窗格貼回檢視畫面

由於 .SwipeReveal-panel 內的按鈕或連結在靜止時位於螢幕外,因此使用鍵盤切換至這些按鈕或連結時,系統會自動捲動 .SwipeReveal,顯示焦點控制項。不過,由於 .SwipeReveal-content 橫跨外部容器寬度的 100%,因此即使側邊面板開啟,大部分內容仍會留在捲動區塊內。因此,按下 Shift+Tab 將焦點移回 .SwipeReveal-content (或其中的可聚焦元素) 時,不會自動將軌道捲動回 0。

監聽 .SwipeReveal-content 上的 focusin 並檢查 event.target.matches(':focus-visible'),讓鍵盤焦點在窗格或任何子系上回到 .SwipeReveal-content 的開頭,而不會干擾指標點擊或觸控拖曳:

for (const content of document.querySelectorAll('.SwipeReveal-content')) {
  content.addEventListener('focusin', (event) => {
    if (event.target.matches(':focus-visible')) {
      content.scrollIntoView({
        block: 'nearest',
        inline: 'start',
      });
    }
  });
}

最佳做法和常見問題

  • 務必使用 scroll-snap-type: x mandatory,而非 proximity。使用 proximity 時,輕輕滑動可讓動作按鈕或側邊選單在靜止時部分露出。
  • 建議在外層水平滑動顯示捲動器 (.SwipeReveal) 上設定 overscroll-behavior: chain,這樣捲動動作就能鏈結至父項捲動器,而不會導致顯示的選單或動作條跳動。
  • 務必在內部可捲動內容窗格 (.SwipeReveal-content.is-scrollable) 上保留 overscroll-behavior: auto (預設),以便在水平滑動手勢仍鏈結至外部滑動捲動器時,提供正常的彈跳回饋。
  • 務必將顯示的動作和側邊選單項目,實作為可聚焦的實際 DOM 控制項 (<button>、<a>)。如果 .SwipeReveal-content 可聚焦或包含可聚焦的控制項,請將 focusin 監聽器與 event.target.matches(':focus-visible') 配對,以呼叫 scrollIntoView({ block: 'nearest', inline: 'start' }),這樣一來,透過 Tab 鍵返回內容窗格時,側邊面板就會關閉,不會影響指標互動。
  • 請勿在 JavaScript 中攔截 wheel、touchmove 或 pointermove 事件,手動翻譯內容或模擬捲動鏈結。主執行緒手勢攔截會封鎖合成器驅動的捲動、破壞原生捲動動量,並降低回應速度。

備用

當 .SwipeReveal 容器將 .SwipeReveal-panel 放在 .SwipeReveal-content 左側 (inline-start) 時,不支援 scroll-initial-target: nearest 的瀏覽器一開始會以捲動偏移 0 算繪,並顯示前置面板。

如果基準目標不支援 scroll-initial-target,請使用 CSS.supports('scroll-initial-target', 'nearest') 偵測支援,並在 load 上使用 behavior: 'instant' 呼叫 scrollTo(),這樣含有前置面板的容器就會直接跳到 .SwipeReveal-content,而不會捲動頁面:

漸進增強

將 overscroll-behavior: chain 和 scrollbar-width: none 視為漸進增強功能,不使用 JavaScript 備用方案:

  • 自動 CSS 階層備用:overscroll-behavior 的初始值為 auto。尚未辨識 chain 關鍵字的瀏覽器會在剖析時忽略 overscroll-behavior: chain,並保留 auto。由於 auto 已允許捲動鏈結至祖系捲動容器,因此滑動顯示手勢和捲動鏈結至父項的功能,在所有瀏覽器中都能立即運作;支援的瀏覽器只會抑制外部滑動包裝函式上不必要的橡皮筋彈跳效果。
  • 覆寫 contain 或 none:如果基礎樣式表或公用程式類別設定 overscroll-behavior: contain 或 none,請在 overscroll-behavior: chain 之前宣告 overscroll-behavior: auto,這樣尚未支援 chain 的瀏覽器仍會將捲動鏈結至父項,而不是將其設為陷阱。
  • 隱藏捲軸 (scrollbar-width: none):隱藏水平捲軸軌道純粹是為了美觀;如果舊版引擎忽略 scrollbar-width: none,滑動顯示容器仍可搭配標準捲軸正常運作。