実際のアニメーションを見てみよう
以下のプレビューをスクロールして、見出しが下からスライドアップし、サブタイトルが1文字ずつ表示される様子を確認してみてください。
スクロールアニメーションの仕組み
Intersection Observer APIの特徴
- 高パフォーマンス スクロールイベントを毎回処理するより、はるかに効率的
- 閾値の柔軟な設定 要素が何%画面に入ったらトリガーするかを自由に指定可能
- 複数要素の一括監視 一つの監視機能で複数の要素を同時に管理できる
- 自動的な監視解除 一度アニメーションさせたら監視を停止し、リソースを節約
Intersection Observer APIの基本
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 少し遅延させてからアニメーション開始
setTimeout(() => {
entry.target.classList.add('visible');
}, 200);
// 一度アニメーションしたら監視を解除
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 }); // 50%見えたらトリガー
const sections = document.querySelectorAll('.scroll-section');
sections.forEach(section => observer.observe(section)); 2つのアニメーション技法
見出しのスライドアップ技法
見出しのアニメーションでは、外側の要素で文字を隠し、内側の要素をスライドさせます。
- 外側の要素で隠す
overflow: hiddenで文字がはみ出さないようにする - 内側の要素を下に配置
spanタグで囲んだ文字をtranslateY(100%)で完全に隠す - アニメーションで浮上
translateY(0)に戻すことで、下から文字がせり上がる
見出しのアニメーション
/* 外側の要素でマスク */
.scroll-heading {
overflow: hidden;
display: inline-block;
}
/* 内側の要素を下に配置 */
.scroll-heading span {
display: block;
transform: translateY(100%); /* 完全に下に隠す */
}
/* アニメーション開始 */
.scroll-section.visible .scroll-heading span {
animation: headingSlideUp 0.8s ease-out forwards;
}
@keyframes headingSlideUp {
to {
transform: translateY(0); /* 元の位置に戻る */
}
} サブタイトルのタイピング風アニメーション
サブタイトルのアニメーションでは、steps()関数で1文字ずつ表示します。
- 初期状態を隠す
width: 0で文字を完全に隠す - 折り返しを防ぐ
white-space: nowrapで1行に保つ - 1文字ずつ表示
steps(50)で50段階に分けて表示し、タイピング風の動きを実現
サブタイトルのアニメーション
.scroll-subtitle {
overflow: hidden;
white-space: nowrap;
width: 0; /* 初期幅0 */
}
.scroll-section.visible .scroll-subtitle {
animation: subtitleReveal 2s steps(50) 0.6s forwards;
}
@keyframes subtitleReveal {
to { width: 100%; }
} AIでこのパーツを作成する
AIへのプロンプト例
プロンプト
スクロールに連動した見出しアニメーションを作成してください。
要件:
- 見出しは下から徐々にスライドアップして表示
- サブタイトルは左から右へ1文字ずつタイピング風に表示。カーソル点滅は不要。
- 見出しとサブタイトルのアニメーション開始を少しずらす
- スクロールで画面内に入ったらアニメーション開始
重要なコードポイント
見出しのスライドアップ
見出しのアニメーション構造
/* 外側の要素でマスク */
.scroll-heading {
overflow: hidden; /* はみ出しを隠す */
display: inline-block; /* 幅を内容に合わせる */
}
/* 内側の要素を下に配置 */
.scroll-heading span {
display: block;
transform: translateY(100%); /* 完全に下に隠す */
}
/* アニメーション開始 */
.scroll-section.visible .scroll-heading span {
animation: headingSlideUp 0.8s ease-out forwards;
}
@keyframes headingSlideUp {
to {
transform: translateY(0); /* 元の位置に戻る */
}
} - overflow: hidden 外側の要素がマスクとなり、文字を隠す
- translateY(100%) spanタグで囲んだ文字を完全に下に配置
- ease-out 終わりに向けて減速し、自然な動きに
サブタイトルのタイピング効果
サブタイトルのアニメーション構造
.scroll-subtitle {
overflow: hidden; /* はみ出しを隠す */
white-space: nowrap; /* 改行しない */
width: 0; /* 初期幅0 */
}
.scroll-section.visible .scroll-subtitle {
animation: subtitleReveal 2s steps(50) 0.6s forwards;
}
@keyframes subtitleReveal {
to { width: 100%; } /* 幅を100%に */
} - steps(50) 50段階に分けて表示し、1文字ずつ表示されるタイピング風の動きに
- 0.6s遅延 見出しの後に自然に続く
- 文字数より多めに設定 確実に1文字ずつ表示される効果を得る
Intersection Observerの設定
スクロール検出
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add('visible');
}, 200);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 }); // 50%見えたらトリガー - threshold: 0.5 要素の50%が画面に入ったらトリガー
- setTimeout 200ms スクロールが落ち着いてからアニメーション開始
- unobserve 一度アニメーションしたら監視を解除し、パフォーマンスを向上
実際のWebサイトでの実装
実装手順
- HTMLにセクションを配置
scroll-sectionクラスを持つセクションを作成し、その中に見出し(spanで囲む)とサブタイトルを配置
- CSSで初期状態を設定
- 見出しはoverflow:hiddenでマスク、spanを
translateY(100%)で下に配置 - サブタイトルは
width: 0で非表示にする
- 見出しはoverflow:hiddenでマスク、spanを
- CSSでアニメーションを定義
- 見出しは
headingSlideUpでtranslateY(0)に戻す - サブタイトルは
subtitleRevealでwidth: 100%に展開、steps(50)でタイピング風に
- 見出しは
- JavaScriptでIntersection Observerを設定
- 対象のセクションを監視し、画面内に入ったら
visibleクラスを追加 - threshold: 0.5、200msの遅延を設定
- 対象のセクションを監視し、画面内に入ったら
- visibleクラスでアニメーション開始
- CSSで
.visibleが付いた時にアニメーションを実行するよう設定
- CSSで
完全な実装コード
実際のWebサイトに組み込むための完全なコードです。
完全なHTML/CSS/JavaScript
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>スクロールアニメーション</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', sans-serif;
}
/* スペーサー(スクロール確認用) */
.spacer {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
font-size: 1.5rem;
color: #9ca3af;
}
.spacer-full {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
font-size: 1.5rem;
color: #9ca3af;
}
.scroll-section {
text-align: left;
padding: 60px 20px;
background: #ffffff;
}
/* 見出し:外側の要素でマスク */
.scroll-heading {
font-size: 2.5rem;
font-weight: bold;
color: #1f2937;
overflow: hidden;
display: inline-block;
margin-bottom: 4px;
}
/* 見出し:内側の要素を下に配置 */
.scroll-heading span {
display: block;
transform: translateY(100%);
}
/* サブタイトル:初期状態 */
.scroll-subtitle {
display: inline-block;
font-size: 1rem;
color: #6b7280;
letter-spacing: 0.1em;
overflow: hidden;
white-space: nowrap;
width: 0;
margin: 0;
}
/* アニメーション定義:見出しを下から上へスライド */
@keyframes headingSlideUp {
to {
transform: translateY(0);
}
}
/* アニメーション定義:サブタイトルを左から右へ展開 */
@keyframes subtitleReveal {
to {
width: 100%;
}
}
/* visible時にアニメーション開始 */
.scroll-section.visible .scroll-heading span {
animation: headingSlideUp 0.8s ease-out forwards;
}
.scroll-section.visible .scroll-subtitle {
animation: subtitleReveal 2s steps(50) 0.6s forwards;
}
</style>
</head>
<body>
<div class="spacer-full">
<p>下にスクロールしてください</p>
</div>
<section class="scroll-section">
<h2 class="scroll-heading"><span>サービス紹介</span></h2>
<p class="scroll-subtitle">Our Services</p>
</section>
<div class="spacer">
<p>さらに下へスクロール</p>
</div>
<section class="scroll-section">
<h2 class="scroll-heading"><span>お客様の声</span></h2>
<p class="scroll-subtitle">Customer Reviews</p>
</section>
<div class="spacer">
<p>もう少し下へ</p>
</div>
<section class="scroll-section">
<h2 class="scroll-heading"><span>お問い合わせ</span></h2>
<p class="scroll-subtitle">Contact Us</p>
</section>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 少し遅延させてからアニメーション開始
setTimeout(() => {
entry.target.classList.add('visible');
}, 200);
// 一度アニメーションしたら監視を解除
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 }); // 50%見えたらトリガー
// すべての.scroll-sectionを監視
document.querySelectorAll('.scroll-section').forEach(section => {
observer.observe(section);
});
</script>
</body>
</html> カスタマイズのヒント
調整できるポイント
- スライド距離
translateY(100%)を50%にすると浅く、150%にすると深くスライド - アニメーション速度 見出しの
0.8s、サブタイトルの2sを調整(小さくすると速く) - ステップ数
steps(50)の値を変更(文字数より多めに設定すると1文字ずつ表示される) - 開始タイミング サブタイトルの
0.6s遅延を調整(見出しとの間隔を変更) - threshold
0.5を0.3にすると早めにトリガー、0.7にすると遅めに
- スライド距離
translateY(100%)を50%にすると浅く、150%にすると深くスライド - アニメーション速度 見出しの
0.8s、サブタイトルの2sを調整(小さくすると速く) - ステップ数
steps(50)の値を変更(文字数より多めに設定すると1文字ずつ表示される) - 開始タイミング サブタイトルの
0.6s遅延を調整(見出しとの間隔を変更) - threshold
0.5を0.3にすると早めにトリガー、0.7にすると遅めに
パフォーマンスの考慮
多くの要素にアニメーションを適用する場合は、transformプロパティのアニメーションはGPUアクセラレーションが効いてスムーズに動作します。widthのアニメーションは再レイアウトが発生しますが、overflow: hiddenとの組み合わせで最小限に抑えられます。要素数が非常に多い場合は、transform: scaleX()への変更も検討できます。
まとめ
スクロールアニメーション活用のポイント
- overflow-hidden技法 見出しを
spanで囲み、外側でマスクして下からスライドアップ - steps()関数 サブタイトルは
steps(50)で1文字ずつ表示されるタイピング風の動き - width展開アニメーション
width: 0から100%へ変化させ、左から右へ文字が現れる - Intersection Observer スクロール検出にはこのAPIを使用し、パフォーマンスを確保
- タイミングのずれ 複数要素のアニメーションは開始タイミングをずらして自然な流れに(0.6s遅延)
- threshold設定 0.5で50%表示時にトリガー、200msの遅延で落ち着いた動きに
- forwards アニメーション終了後の状態を維持するために指定
- 一度だけ実行
unobserve()で同じ要素のアニメーションが繰り返されないように