はじめに:なぜスクロールベースのストーリーテリングなのか
私たちが日常的に行う「スクロール」という動作は、単にページを進める以上の可能性を秘めています。特に広告、ブランドストーリー、ポートフォリオサイトにおいて、ユーザーの没入感を高める強力なツールとして注目されています。本記事では、CSSのscroll-snapプロパティとJavaScriptのscrollsnapchanging、scrollsnapchangeイベントを活用して、スクロール1回でシーンが切り替わるインタラクティブカードの実装方法を紹介します。
本チュートリアルは、2026年の母の日に向けて制作された実験的プロジェクトから着想を得ています。単なる技術実装を超え、ユーザー体験を考慮した設計方法まで一緒に学んでいきましょう。
参考資料: UCP(Universal Commerce Protocol)完全ガイド:エージェントコマースの標準が開かれるで解説されているエージェントベースUI設計の原則とも通じる部分がありますので、合わせてご参照ください。

本論:CSS scroll-snap eventsでシーン切り替えを実装する
1. 基本的なHTML構造
まずスクロールスナップが適用されるコンテナと各シーン(パネル)を用意します。
<!-- スクロールスナップコンテナ -->
<div id="snap-scroller">
<section class="snap-panel" id="day-panel">
<!-- 昼のシーンコンテンツ -->
<h2>☀️ 昼</h2>
<p>母と過ごした思い出の中へ...</p>
</section>
<section class="snap-panel" id="night-panel">
<!-- 夜のシーンコンテンツ -->
<h2>🌙 夜</h2>
<p>星の下で思い出をたどる</p>
</section>
</div>
2. CSS:scroll-snapの設定
スクロールコンテナにscroll-snap-typeを、各パネルにscroll-snap-alignを指定します。scroll-snap-stop: alwaysはユーザーがパネルの途中で止まれないように強制します。
/* スクロールコンテナ */
#snap-scroller {
overflow-y: auto;
scroll-snap-type: y mandatory;
height: 100vh; /* ビューポート全体の高さ */
}
/* 各スナップターゲット */
.snap-panel {
scroll-snap-align: start;
scroll-snap-stop: always;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: background-color 0.5s ease;
}
#day-panel {
background: linear-gradient(135deg, #f9d423, #ff4e50);
}
#night-panel {
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
color: white;
}
3. JavaScript:scroll-snap eventsで動作を制御
scrollsnapchangingイベントはユーザーがスクロールしている間に次にスナップされるパネルを教えてくれます。scrollsnapchangeはスナップが完了した後の最終パネルを通知します。この2つのイベントを組み合わせることで、シーン遷移中と遷移後のロジックを分離できます。
const snapScroller = document.getElementById('snap-scroller');
const dayPanel = document.getElementById('day-panel');
const nightPanel = document.getElementById('night-panel');
let selectedPanel = null; // 現在スナップされているパネル
// スクロール中、次に近づいているパネルが変わるたびに実行
snapScroller.addEventListener('scrollsnapchanging', ({ snapTargetBlock }) => {
// snapTargetBlock: ユーザーが向かっているパネル要素
if (snapTargetBlock === dayPanel) {
onScrollingTowardDay();
}
if (snapTargetBlock === nightPanel) {
onScrollingTowardNight();
}
});
// スナップが完了してパネルが固定されたら実行
snapScroller.addEventListener('scrollsnapchange', ({ snapTargetBlock }) => {
selectedPanel = snapTargetBlock;
if (snapTargetBlock === dayPanel) {
onLandedOnDay();
}
if (snapTargetBlock === nightPanel) {
onLandedOnNight();
}
});
// コールバック関数の例
function onScrollingTowardDay() {
console.log('昼パネルにスクロール中...');
// 例:UFOのランダムパス生成開始
}
function onScrollingTowardNight() {
console.log('夜パネルにスクロール中...');
// 例:テキスト物理エフェクト有効化
}
function onLandedOnDay() {
console.log('昼パネルに到着!');
// 例:昼関連のアニメーション再生
}
function onLandedOnNight() {
console.log('夜パネルに到着!');
// 例:流れ星エフェクト開始
}
注意点:
scrollsnapchangingとscrollsnapchangeは2026年現在、ChromeおよびOperaベースのブラウザでのみ動作します。FirefoxやSafariではポリフィルや代替動作を準備する必要があります。実際のサービスに適用する際は、@supportsまたは機能検出を通じてプログレッシブエンハンスメント(段階的機能向上)を適用してください。

発展:ランダム要素との組み合わせによる動的体験
上記の基本構造にランダムなUFOの軌道やテキスト反発エフェクトを追加すると、より生き生きとしたインタラクションが可能になります。例えば、scrollsnapchangingイベントでUFOの開始位置と目標位置をランダムに設定し、CSSアニメーションで滑らかに移動させることができます。
// UFOランダムパス生成の例
function createRandomPath() {
const startX = Math.random() * window.innerWidth;
const startY = Math.random() * window.innerHeight * 0.3;
const endX = Math.random() * window.innerWidth;
const endY = Math.random() * window.innerHeight * 0.7;
return { startX, startY, endX, endY };
}
// scrollsnapchangingコールバックで呼び出し
function onScrollingTowardNight() {
const path = createRandomPath();
// UFO要素にCSSカスタムプロパティとして渡す
document.getElementById('ufo').style.setProperty('--start-x', path.startX + 'px');
document.getElementById('ufo').style.setProperty('--start-y', path.startY + 'px');
document.getElementById('ufo').style.setProperty('--end-x', path.endX + 'px');
document.getElementById('ufo').style.setProperty('--end-y', path.endY + 'px');
}
/* UFOアニメーション */
#ufo {
position: absolute;
width: 60px;
height: 40px;
background: radial-gradient(circle, #fff, #aaa);
border-radius: 50%;
animation: fly 3s ease-in-out infinite;
transition: transform 0.5s;
}
@keyframes fly {
0% {
transform: translate(var(--start-x), var(--start-y));
}
100% {
transform: translate(var(--end-x), var(--end-y));
}
}
このようにscroll-snap eventsは、単なるスクロール固定を超えて、ユーザーのスクロール意図に応じて動的に反応する体験を設計することを可能にします。特に広告ランディングページ、製品紹介サイト、インタラクティブストーリーなどでの活用が期待されます。

まとめ:実務適用のアドバイス
CSS scroll-snap eventsはまだ実験的な技術ですが、その可能性は無限大です。ただし、以下の点に必ず留意してください。
- ブラウザ互換性: Chrome/Opera以外ではポリフィルや代替UXを準備しましょう。
scroll-behavior: smoothと組み合わせれば、基本的なスクロールの滑らかさは保証できます。 - アクセシビリティ: スクロールベースのナビゲーションは、キーボードや支援技術を使用するユーザーにとって不便な場合があります。
tabindexやaria属性を適切に追加し、スクロール以外にもボタンクリックでシーンを切り替えられる代替手段を提供してください。 - パフォーマンス: scroll-snap eventsはスクロール中に頻繁に発生するため、コールバック関数内で重い処理を避け、
requestAnimationFrameを活用しましょう。
合わせて読みたい記事
- Reactの未来が変わる:独立財団発足と新しいテクノロジーガバナンス – Web技術のガバナンス変化が開発者に与える意味
次のステップとしての学習方向
- CSS
scroll-snap-type: bothを使用した横/縦複合スクロール - Intersection Observer APIを活用した代替実装
- Three.jsやGSAPと組み合わせた3Dスクロールストーリーテリング
母の日カードのような個人的なプロジェクトにこの技術を適用することで、単にコードを追うだけでなく、ユーザーに感動を与えるインタラクションを設計する感覚を養えるでしょう。Happy coding!