AI先生のロボットキャラクター
第9章 - セクション2

見出しとサブタイトルが別々に動くスクロールアニメーション

見出しが下からスライドアップし、サブタイトルが左から右へ展開されるスクロール連動アニメーション

男子生徒のアイコン

先生、ランディングページでよく見る、スクロールすると見出しが下から浮き上がってくるアニメーションってどうやって作るんですか?

AI先生のアイコン

いいね!見出しが下からスライドアップして、サブタイトルが1文字ずつタイピング風に表示されるアニメーションを作ってみよう。まずは実際に動きを見てもらった方が早いかな。

女子生徒のアイコン

実際に見てみたいです!

AI先生のアイコン

じゃあ、下のプレビューをスクロールして確認してみて。


実際のアニメーションを見てみよう

以下のプレビューをスクロールして、見出しが下からスライドアップし、サブタイトルが1文字ずつ表示される様子を確認してみてください。

男子生徒のアイコン

すごい!見出しが下から浮き上がって、サブタイトルが1文字ずつタイピングされてる!

女子生徒のアイコン

これ、どうやって作るんですか?

AI先生のアイコン

順番に説明していくね。まずスクロール検出の仕組みから見ていこう。


スクロールアニメーションの仕組み

男子生徒のアイコン

スクロールで要素が画面に入ったことって、どうやって検出するんですか?

AI先生のアイコン

Intersection Observer APIという仕組みを使うよ。これは要素が画面内に入ったかどうかを効率的に監視できるブラウザの標準機能なんだ。

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つのアニメーション技法

女子生徒のアイコン

見出しとサブタイトルで動きが違いますけど、それぞれどうやって作るんですか?

AI先生のアイコン

見出しはoverflow-hiddenマスク技法、サブタイトルはsteps()関数によるタイピング風アニメーションを使っているんだ。

見出しのスライドアップ技法

見出しのアニメーションでは、外側の要素で文字を隠し、内側の要素をスライドさせます。

  • 外側の要素で隠す 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%; }
}
男子生徒のアイコン

steps(50)がタイピング風のカクカクした動きを作ってるんですね!

AI先生のアイコン

そういうこと。文字数より多めに設定することで、1文字ずつ表示される効果が得られるよ。


AIでこのパーツを作成する

男子生徒のアイコン

これってAIに頼んで作ってもらえますか?

AI先生のアイコン

もちろん。イメージを伝えれば、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サイトでの実装

男子生徒のアイコン

プレビューではすぐにアニメーションが始まりますが、実際のサイトではスクロールしたときに動くんですよね?

AI先生のアイコン

その通り。実際のサイトでは、JavaScriptでスクロールを検出して、要素が画面内に入った時にアニメーションを開始するんだ。

実装手順

  • HTMLにセクションを配置
    • scroll-sectionクラスを持つセクションを作成し、その中に見出し(spanで囲む)とサブタイトルを配置
  • CSSで初期状態を設定
    • 見出しはoverflow:hiddenでマスク、spanをtranslateY(100%)で下に配置
    • サブタイトルはwidth: 0で非表示にする
  • CSSでアニメーションを定義
    • 見出しはheadingSlideUptranslateY(0)に戻す
    • サブタイトルはsubtitleRevealwidth: 100%に展開、steps(50)でタイピング風に
  • JavaScriptでIntersection Observerを設定
    • 対象のセクションを監視し、画面内に入ったらvisibleクラスを追加
    • threshold: 0.5、200msの遅延を設定
  • visibleクラスでアニメーション開始
    • CSSで.visibleが付いた時にアニメーションを実行するよう設定

完全な実装コード

実際の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>

カスタマイズのヒント

女子生徒のアイコン

このアニメーション、色や速度を変えたりできますか?

AI先生のアイコン

もちろん。いくつかのポイントを調整するだけで、印象が大きく変わるよ。

調整できるポイント

  • スライド距離 translateY(100%)を50%にすると浅く、150%にすると深くスライド
  • アニメーション速度 見出しの0.8s、サブタイトルの2sを調整(小さくすると速く)
  • ステップ数 steps(50)の値を変更(文字数より多めに設定すると1文字ずつ表示される)
  • 開始タイミング サブタイトルの0.6s遅延を調整(見出しとの間隔を変更)
  • threshold 0.50.3にすると早めにトリガー、0.7にすると遅めに
  • スライド距離 translateY(100%)を50%にすると浅く、150%にすると深くスライド
  • アニメーション速度 見出しの0.8s、サブタイトルの2sを調整(小さくすると速く)
  • ステップ数 steps(50)の値を変更(文字数より多めに設定すると1文字ずつ表示される)
  • 開始タイミング サブタイトルの0.6s遅延を調整(見出しとの間隔を変更)
  • threshold 0.50.3にすると早めにトリガー、0.7にすると遅めに
パフォーマンスの考慮

多くの要素にアニメーションを適用する場合は、transformプロパティのアニメーションはGPUアクセラレーションが効いてスムーズに動作します。widthのアニメーションは再レイアウトが発生しますが、overflow: hiddenとの組み合わせで最小限に抑えられます。要素数が非常に多い場合は、transform: scaleX()への変更も検討できます。


まとめ

男子生徒のアイコン

見出しとサブタイトルで別々の動きをさせるのって、意外とシンプルにできるんですね!

AI先生のアイコン

そうだね。ポイントは「それぞれの要素に異なるアニメーションを設定する」ことと「タイミングをずらす」ことなんだ。見出しはoverflow-hiddenマスク技法でスライド、サブタイトルはwidth展開で異なる動きを実現しているよ。

女子生徒のアイコン

1文字ずつ表示されるタイピング風のアニメーションは、ランディングページのセクション見出しにぴったりですね!

AI先生のアイコン

Intersection Observer APIを使えば、スクロールに連動したアニメーションが効率的に実装できるよ。threshold設定で表示タイミングを調整できるのも便利だね。ぜひ自分のサイトでも試してみてね。

スクロールアニメーション活用のポイント
  • overflow-hidden技法 見出しをspanで囲み、外側でマスクして下からスライドアップ
  • steps()関数 サブタイトルはsteps(50)で1文字ずつ表示されるタイピング風の動き
  • width展開アニメーション width: 0から100%へ変化させ、左から右へ文字が現れる
  • Intersection Observer スクロール検出にはこのAPIを使用し、パフォーマンスを確保
  • タイミングのずれ 複数要素のアニメーションは開始タイミングをずらして自然な流れに(0.6s遅延)
  • threshold設定 0.5で50%表示時にトリガー、200msの遅延で落ち着いた動きに
  • forwards アニメーション終了後の状態を維持するために指定
  • 一度だけ実行 unobserve()で同じ要素のアニメーションが繰り返されないように

学習チェック

このレッスンを理解できたら「完了」をクリックしてください。
後で見直したい場合は「未完了に戻す」で進捗をリセットできます。

レッスン完了!🎉

お疲れさまでした!