実際のアニメーションを見てみよう
以下のプレビューをスクロールして、見出しが下からスライドアップし、サブタイトルが1文字ずつ表示される様子を確認してみてください。
HTML
<!-- 下にスクロールしてくださいは画面いっぱいに表示 -->
<div class="page-container">
<section class="spacer-full">
<p>下にスクロールしてください</p>
</section>
<section class="scroll-section" id="section1">
<h2 class="scroll-heading"><span>サービス紹介</span></h2>
<p class="scroll-subtitle">Our Services</p>
</section>
<section class="spacer">
<p>さらに下へスクロール</p>
</section>
<section class="scroll-section" id="section2">
<h2 class="scroll-heading"><span>お客様の声</span></h2>
<p class="scroll-subtitle">Customer Reviews</p>
</section>
<section class="spacer">
<p>もう少し下へ</p>
</section>
<section class="scroll-section" id="section3">
<h2 class="scroll-heading"><span>お問い合わせ</span></h2>
<p class="scroll-subtitle">Contact Us</p>
</section>
</div> CSS
.page-container {
height: 100%;
overflow-y: auto;
}
.spacer-full {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
color: #9ca3af;
font-size: 1.2rem;
}
.spacer {
height: 260px;
display: flex;
align-items: center;
justify-content: center;
background: #f9fafb;
color: #9ca3af;
font-size: 1rem;
}
.scroll-section {
padding: 80px 20px;
background: #ffffff;
}
.scroll-section-inner {
max-width: 960px;
margin: 0 auto;
}
/* 見出し:初期状態(下に隠れている) */
.scroll-heading {
font-size: 2rem;
font-weight: bold;
color: #1f2937;
margin-bottom: 4px;
overflow: hidden;
display: inline-block;
}
.scroll-heading span {
display: block;
transform: translateY(100%);
}
/* サブタイトル:初期状態(左に隠れている・1文字ずつ表示) */
.scroll-subtitle {
font-size: 1rem;
color: #6b7280;
letter-spacing: 0.05em;
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;
} JavaScript
// 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%見えたらトリガー
});
// すべてのscroll-sectionを監視
const sections = document.querySelectorAll('.scroll-section');
sections.forEach(section => observer.observe(section)); スクロールアニメーションの仕組み
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%; }
} 重要なコードポイント
見出しのスライドアップ
見出しのアニメーション構造
/* 外側の要素でマスク */
.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の設定
スクロール検出
// 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%見えたらトリガー
});
// すべてのscroll-sectionを監視
const sections = document.querySelectorAll('.scroll-section');
sections.forEach(section => observer.observe(section)); - 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で
カスタマイズのヒント
調整できるポイント
- スライド距離
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()で同じ要素のアニメーションが繰り返されないように