目次
情報確認日:2026年10月16日。GSAP公式資料を参照した、本サイト独自の実装例です。
Observerで横方向のドラッグを検出し、カードを切り替える例です。ページの縦スクロールを妨げない範囲、ボタンでの代替操作、境界判定、無効化時の後片付けを具体的に説明します。
スポンサーリンク
ObserverとIntersection Observerは用途が異なる
GSAPのObserverは、タッチやポインターなどの入力を扱うプラグインです。要素が画面内に入ったかを監視するブラウザー標準のIntersection Observerとは異なります。名前が似ていても、この例で観測するのはカードに対する入力操作です。
既存の横スクロール記事がスクロール位置から進行度を求めるのに対し、今回は一回の横操作を一枚の切り替えへ変換します。ページ全体のスクロール量をアニメーションへ連動させません。スライダー製品のすべての機能を置き換える完成品ではなく、入力と表示状態を分けて学ぶ例です。
JavaScriptが動く前はすべて読める状態にする
HTMLでは三枚のカードを通常の縦並びにし、操作ボタンはhiddenにしています。初期表示をCSSで隠し、JavaScriptが成功するまで情報が読めない構成にはしません。初期化時にenhancedを付けた場合だけ横並びに変えます。
横並びのトラックは表示領域と同じ幅のままにし、各カードをその100%幅へ固定します。xPercentを一枚につき100ずつ移動させる計算は、このCSSの前提と一組です。トラック自体をカード枚数倍の幅へ変更すると、百分率の基準が変わるため同じ計算を流用できません。
スポンサーリンク
入力を受ける領域を限定する
Observerのtargetをviewportへ絞り、typeをtouch,pointerにしています。ホイールをページ全体で捕まえて、縦方向の入力を横移動へ置き換える構成ではありません。touch-action:pan-yでブラウザーの縦スクロールを許可し、preventDefaultもfalseにします。
これでも、あらゆる端末や埋め込み先で操作競合が起きない保証にはなりません。縦方向の操作をブラウザーが引き取ると、アプリ側がすべての移動を受け取れない場合もあります。縦スクロール中に確実にカードも切り替えることは目標にせず、操作を奪わないことを優先します。
横方向の意図を確認してから一枚進める
押した位置を保存し、離した時点との差を比較します。横の移動が48 CSSピクセルに満たない場合、または縦の移動以上でない場合は切り替えません。この値は本サイトの例であり、GSAPが推奨する一律のしきい値ではありません。
dragMinimumはドラッグとして認識するまでの距離に関わる設定で、カードを進める独自条件とは役割を分けています。lockAxisも入力の方向を扱うための設定であり、ページのスクロール制御を丸ごと代行するものではありません。小さな誤操作が切り替えへ直結しないよう、離した時点で一度だけ判断します。
スポンサーリンク
ボタン操作を同じ状態更新へ集約する
前へ・次へのボタンとスワイプは同じshowを呼びます。先頭と末尾で値を範囲内へ収め、対応するボタンを無効化します。表示状態を一か所で決めれば、入力手段ごとに違うページ番号になる問題を避けやすくなります。
標準のbuttonはキーボードでも操作できます。スワイプだけに頼らず、マウスやキーボードで同じ情報へ進めるようにします。スライド内に操作要素を置く場合は、フォーカスが残ったまま非表示になる経路がないか追加検討が必要です。この例のカードにはリンクや入力欄を置いていません。
見えないカードを操作対象へ残さない
表示しないカードにはinertとaria-hiddenを設定しています。見切れたカードへキーボードのフォーカスが入ったり、視覚上の現在位置と読み上げ対象がずれたりする問題を減らすためです。対象ブラウザーでinertの扱いを確認し、必要ならプロジェクトの対応方針を決めます。
状態の案内はrole=statusの文章で行い、ボタンを押すたびにフォーカスをカードへ強制移動しません。自動再生もしません。カードの内容量が異なる場合や複雑な操作がある場合は、この簡易例だけでカルーセルのアクセシビリティが完成したとは判断しないでください。
画面を外すときは監視とTweenを止める
入力監視はobserver.killで停止し、ctx.revertでアニメーションによる変更を戻します。後からボタン操作で作られるTweenもコンテキストへ記録されるよう、ctx.addでshowを定義しています。単に初期化処理をcontextで囲むだけでは、将来のすべてのイベント処理が自動で対象になるわけではありません。
さらにイベントリスナーを外し、inertとaria-hiddenを解除して、通常の縦並びへ戻します。コンテキストの復元は自分で追加したすべてのHTML属性やイベントを代行しません。画面の再表示を伴う環境では、離脱と再初期化を一組で設計します。
実端末で縦と横を別々に確かめる
検証では、ボタンの連打、両端での操作、短い横移動、長い縦移動、斜めの操作を分けます。動きを減らす設定では時間を0にし、現在位置は同じルールで更新します。幅が変わっても一枚分の移動が正しいか確認します。
実際のタッチ操作、ブラウザーのスクロール引き継ぎ、読み上げやフォーカスの挙動は模擬DOMだけでは評価できません。表示するカードが多い場合、画像の遅延読み込みや高さの変化も追加の課題になります。必要な機能が多いなら、既存のアクセシブルなコンポーネントの採用も比較してください。
スポンサーリンク
実装例と配置するファイル
npmパッケージを解決できる既存のViteなどの環境で npm install gsap を実行してください。HTMLはbody内、CSSはスタイルシート、JavaScriptはmain.jsへ置き、body末尾からtype="module"でmain.jsを読み込みます。WordPress本文でコードを実行する例ではなく、開発環境で動かすためのサンプルです。
<section class="carousel" aria-label="サービスの紹介">
<div class="viewport"><div class="slides">
<article class="panel"><h2>設計</h2><p>要件を整理します。</p></article>
<article class="panel"><h2>実装</h2><p>小さく作って確かめます。</p></article>
<article class="panel"><h2>運用</h2><p>継続して改善します。</p></article>
</div></div>
<div class="controls" hidden><button type="button" class="prev">前へ</button><button type="button" class="next">次へ</button><p class="status" role="status"></p></div>
</section>
.carousel { max-width:40rem; margin:2rem auto; padding:1rem; }
.panel { box-sizing:border-box; padding:2rem; background:#eee9fa; margin-bottom:1rem; }
.enhanced .viewport { overflow:hidden; touch-action:pan-y; }
.enhanced .slides { display:flex; }
.enhanced .panel { flex:0 0 100%; min-width:0; margin-bottom:0; }
.controls button { padding:.8rem 1.3rem; margin:.5rem; }
.controls button:focus-visible { outline:3px solid #5125bb; }
import { gsap } from "gsap";
import { Observer } from "gsap/Observer";
gsap.registerPlugin(Observer);
const root = document.querySelector(".carousel");
const viewport = root.querySelector(".viewport");
const track = root.querySelector(".slides");
const panels = [...root.querySelectorAll(".panel")];
const controls = root.querySelector(".controls");
const prev = root.querySelector(".prev");
const next = root.querySelector(".next");
let index = 0, startX = 0, startY = 0;
root.classList.add("enhanced");
controls.hidden = false;
const ctx = gsap.context(() => {}, root);
ctx.add("show", (requested) => {
index = Math.max(0, Math.min(panels.length - 1, requested));
panels.forEach((panel, i) => {
panel.inert = i !== index;
panel.setAttribute("aria-hidden", String(i !== index));
});
prev.disabled = index === 0;
next.disabled = index === panels.length - 1;
root.querySelector(".status").textContent = `${index + 1} / ${panels.length}`;
gsap.to(track, {
xPercent: -100 * index, overwrite: "auto", ease: "power2.out",
duration: matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 0.4
});
});
const observer = Observer.create({
target: viewport, type: "touch,pointer", preventDefault: false,
lockAxis: true, dragMinimum: 10,
onPress(self) { startX = self.x; startY = self.y; },
onRelease(self) {
const dx = self.x - startX, dy = self.y - startY;
if (Math.abs(dx) < 48 || Math.abs(dx) <= Math.abs(dy)) return;
ctx.show(index + (dx < 0 ? 1 : -1));
}
});
const goPrev = () => ctx.show(index - 1);
const goNext = () => ctx.show(index + 1);
prev.addEventListener("click", goPrev);
next.addEventListener("click", goNext);
ctx.show(0);
function destroy() {
observer.kill();
ctx.revert();
prev.removeEventListener("click", goPrev);
next.removeEventListener("click", goNext);
panels.forEach(panel => {
panel.inert = false;
panel.removeAttribute("aria-hidden");
});
controls.hidden = true;
root.classList.remove("enhanced");
}
// SPAなどでこの画面を離れる際はdestroy()を呼びます。
公開前の確認
掲載コードはGSAP 3.15.0を用いたDOM模擬環境で初期化と主要操作を確認しています。実ブラウザーの描画、フォントの改行、タッチの挙動、支援技術による読み上げは別途確認が必要です。開発用の環境だけでなく、対象端末と本番用ビルドで、初期表示・連打・停止・画面の再表示を確認してください。
スポンサーリンク



![JavaScriptモダンプログラミング完全ガイド [堅牢なコードを効率的に開発できる! ]](https://m.media-amazon.com/images/I/91NvEBSfCkL._SL1500_.jpg)