スワイプして表示

スワイプして表示するパターンでは、ユーザーがフォアグラウンドの行またはコンテンツ ペインを水平方向にスライドして、非表示の UI(コンテキストに応じた操作ボタン(固定、ミュート、アーカイブ、削除)やセカンダリ サイドメニューなど)を表示できます。このとき、操作は自動的に有効になりません。スワイプ中にアクションを commit するのではなく、コンテナがスナップして開き、ユーザーが確認して有効にできるインタラクティブなコントロールが表示されます。または、スワイプして閉じることができます。

しきい値を超えたアイテムをすぐに commit して閉じる全幅スワイプ操作については、スワイプして削除をご覧ください。縦スクロール コンテナの上部にある非表示のヘッダーを表示するには、プルして表示をご覧ください。

導入方法

リストアイテムの操作ボタンを表示する場合でも、スクロール可能なペインの横にあるサイドメニューを表示する場合でも、仕組みは同じです。CSS スクロール スナップ(scroll-snap-type: x mandatory)と overscroll-behavior: chain で構成された外側の水平スクロール コンテナ内に、フォアグラウンド コンテンツと非表示パネルをラップします。ブラウザのネイティブ スクロール エンジンは、JavaScript ジェスチャー リスナーを使用せずに、慣性、スナップ、キーボード フォーカス スクロール、スクロール チェーンを処理します。

ステップ 1: スワイプ ラッパー、コンテンツ、表示パネルをマークアップする

.SwipeReveal スクロール コンテナ内に、フォアグラウンド コンテンツを最初に配置し、表示されるコントロール(操作ボタンまたはサイドメニュー)を 2 番目に配置します。コンテンツが 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 の 4 つのキーワードは、過剰なスクロールが祖先スクロール コンテナに伝播するかどうか(スクロール チェーン)と、コンテナがローカルのオーバースクロール アフォーダンス(弾性ゴムバンドの伸縮やエッジグローなど)を表示するかどうか(ローカル境界効果)を個別に制御します。

値 スクロール チェーン(祖先に伝播) ローカル境界効果(ラバーバンド / グロー) スワイプして表示における役割
auto はい はい デフォルトの動作。通常のバウンス フィードバックが必要な内部コンテンツ スクロールではこの設定を維持しますが、メニューがバウンスする原因となる外部スワイプ ラッパーではこの設定を避けます。
chain はい いいえ 外側の .SwipeReveal スクロールバーで使用すると、メニューやアクション ストリップがバウンスすることなく、余分なスクロールが親スクロールバー(ドキュメントなど)にチェーンされます。
contain いいえ はい コンテナ内でスクロールをトラップし、ローカルでバウンスする
none いいえ いいえ ローカル バウンスと祖先へのスクロール チェーンの両方をブロックします。

.SwipeReveal を 2 列のグリッド(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;
  }
}

3 列(grid-template-columns: max-content 100% max-content)または 2 列(max-content 100%)を使用して左側(inline-start)のコントロールも表示する場合は、.SwipeReveal-content の前に先頭の .SwipeReveal-panel を配置します。.SwipeReveal-content に scroll-initial-target: nearest があるため、スクロール コンテナは、最初は先頭パネルではなくコンテンツ列に配置されます。

ステップ 3: キーボード フォーカス時にフォーカス可能なコンテンツ ペインをビューに戻す

.SwipeReveal-panel 内のボタンやリンクは、通常は画面外にあります。キーボードで Tab キーを押してそれらに移動すると、.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',
      });
    }
  });
}

ベスト プラクティスと注意点

  • proximity ではなく scroll-snap-type: x mandatory を使用します。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' }) を呼び出し、コンテンツ ペインにタブを戻すと、ポインタ操作に影響を与えることなくサイドパネルが閉じるようにします。
  • JavaScript で wheel、touchmove、pointermove イベントをインターセプトして、コンテンツを手動で翻訳したり、スクロール チェーンをエミュレートしたりしないでください。メインスレッドのジェスチャー インターセプトは、コンポジタ駆動のスクロールをブロックし、ネイティブ スクロールの慣性を損ない、応答性を低下させます。

フォールバック

.SwipeReveal コンテナが .SwipeReveal-content の前に .SwipeReveal-panel を左(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 を無視した場合、スワイプして表示するコンテナは、標準のスクロールバーで完全に機能します。