スワイプして表示するパターンでは、ユーザーがフォアグラウンドの行またはコンテンツ ペインを水平方向にスライドして、非表示の 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を無視した場合、スワイプして表示するコンテナは、標準のスクロールバーで完全に機能します。