高速スクロール・スピードライン

ゆっくり進んでいたページが突然ヒュンと高速移動し、流線とブラーの残像を残してバウンドしながら停止します。クリスマス特設サイトのような、移動そのものを楽しませるスクロール演出です。

#speed-lines#scroll#motion-blur#dash

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura: 全国ツアー。スクロールで会場から会場へヒュンと高速移動 -->
<div class="stage" data-stage aria-label="ツアー会場を高速移動するスクロール演出">
  <div class="track" data-track>
    <div class="inner">
      <section style="background:#2a0b3f">
        <div class="num">TOKYO</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 8 L61 38 L97 38 L68 60 L79 96 L50 74 L21 96 L32 60 L3 38 L39 38 Z" fill="#FFC2D6"/></svg>
      </section>
      <section style="background:#6a1f6e">
        <div class="num">OSAKA</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 86 C26 64 26 40 50 24 C74 40 74 64 50 86 Z" fill="#FF7DA8"/></svg>
      </section>
      <section style="background:#9B3F8E">
        <div class="num">NAGOYA</div>
        <svg class="icon" viewBox="0 0 100 100"><circle cx="38" cy="62" r="14" fill="#FFD9E0"/><rect x="50" y="20" width="6" height="44" fill="#FFD9E0"/><path d="M50 20 h24 v12 h-24 z" fill="#FFD9E0"/></svg>
      </section>
      <section style="background:#C2455A">
        <div class="num">FUKUOKA</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 14 C66 30 66 58 50 86 C34 58 34 30 50 14 Z" fill="#FFE3B0"/></svg>
      </section>
      <section style="background:#FF6FA8">
        <div class="num">SAPPORO</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 8 L61 38 L97 38 L68 60 L79 96 L50 74 L21 96 L32 60 L3 38 L39 38 Z" fill="#fff"/></svg>
      </section>
    </div>
  </div>
  <div class="lines" data-lines aria-hidden="true"></div>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #1a0626; }

.stage {
  position: relative; width: 100%; height: 100vh;
  min-height: 260px; max-height: 100%; overflow: hidden;
  background: #1a0626; transition: opacity .3s ease;
}
.stage.fading { opacity: 0; }

.track { position: absolute; left: 0; top: 0; width: 100%; will-change: transform; }
.inner { transition: filter .15s ease, transform .15s ease; }
.stage.dashing .inner { filter: blur(4px); transform: scaleY(1.06); }

section { position: relative; height: 100vh; display: grid; place-items: center; overflow: hidden; }
.num {
  font-family: "Arial Black", system-ui, sans-serif; font-weight: 900;
  font-size: clamp(44px, 13vw, 120px); color: rgba(255,255,255,.95); line-height: 1; letter-spacing: .02em;
}
.icon { position: absolute; width: clamp(56px, 13vw, 110px); height: clamp(56px, 13vw, 110px); opacity: .85; }

.lines { position: fixed; inset: 0; pointer-events: none; opacity: 0; transition: opacity .15s ease; z-index: 3; }
.stage.dashing .lines { opacity: 1; }
.line { position: absolute; top: -160px; width: 2px; background: rgba(255,209,224,.6); animation: streak .3s linear infinite; }
@keyframes streak { from { transform: translateY(0); } to { transform: translateY(100vh); } }
JavaScript
// フェーズ制御は WAAPI、translate量は視点高さ基準でスケール(デモと同じロジック)。
(() => {
  const stage = document.querySelector('[data-stage]');
  const track = document.querySelector('[data-track]');
  const lines = document.querySelector('[data-lines]');
  if (!stage || !track || !lines) return;

  for (let i = 0; i < 20; i++) {
    const el = document.createElement('span');
    el.className = 'line';
    el.style.left = ((i * 137.5) % 100) + '%';
    el.style.height = (80 + (i * 53) % 80) + 'px';
    el.style.animationDelay = (-(i % 6) * 0.05) + 's';
    lines.appendChild(el);
  }

  const wait = (ms) => new Promise(r => setTimeout(r, ms));
  const anim = (kf, opts) => track.animate(kf, { fill: 'forwards', ...opts }).finished;
  const ty = (v) => `translateY(${v}px)`;

  async function loop() {
    if (!track.animate) return;
    while (true) {
      const H = stage.clientHeight || 600;
      const creep = -0.33 * H, dash = -3.2 * H, over = dash - 0.12 * H;
      track.style.transform = ty(0);
      stage.classList.remove('fading');
      await anim([{ transform: ty(0) }, { transform: ty(creep) }], { duration: 1200, easing: 'linear' });
      stage.classList.add('dashing');
      await anim([{ transform: ty(creep) }, { transform: ty(dash) }], { duration: 900, easing: 'cubic-bezier(0.6,0,1,1)' });
      stage.classList.remove('dashing');
      await anim([{ transform: ty(dash) }, { transform: ty(over), offset: 0.5 }, { transform: ty(dash) }],
        { duration: 600, easing: 'cubic-bezier(0.34,1.56,0.64,1)' });
      await wait(1000);
      stage.classList.add('fading');
      await wait(300);
    }
  }
  loop();
})();

実装ガイド

使いどころ

イベントやツアー、キャンペーンの特設で、移動そのものを楽しませるスクロール演出に。緩→急の落差で疾走感を出します。

実装時の注意点

フェーズ制御はWAAPI(element.animate)で緩速前進→ダッシュ→着地スプリング→リセットを連結します。translate量は視点高さ基準でスケールさせ環境差に強くします。ダッシュ中だけコンテンツにblur+scaleY、固定配置の流線オーバーレイを表示。blurはコンテンツ側だけに当て、流線には当てません。

対応ブラウザ

Web Animations API・CSS filter/transformは全モダンブラウザで安定動作します。will-changeでダッシュのカクつきを抑え、prefers-reduced-motion時はダッシュを弱める配慮を。対応は実機で確認してください。

よくある失敗

blurを流線にまで当てると残像が濁ります。translateを固定pxにすると画面高さで見え方が破綻するため高さ基準でスケールします。複数タイマー併用はズレるのでフェーズはひとつの連結で扱います。

応用例

セクションを会場や章に、流線の色や本数を調整、実スクロール連動に、効果音と同期(音はサイト方針次第)などに発展できます。

コード

HTML
<!-- 高速スクロール・スピードライン:突然ヒュンと加速し流線とブラーを残して着地 -->
<div class="stage" data-stage aria-label="高速移動するスクロール演出">
  <div class="track" data-track>
    <div class="inner">
      <section style="background:#0F1117">
        <div class="num">01</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 8 L61 38 L97 38 L68 60 L79 96 L50 74 L21 96 L32 60 L3 38 L39 38 Z" fill="#FFC83D"/></svg>
      </section>
      <section style="background:#2B4DA8">
        <div class="num">02</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M64 12 a40 40 0 1 0 24 70 a32 32 0 1 1 -24 -70 Z" fill="#D9F2FF"/></svg>
      </section>
      <section style="background:#5B3FA8">
        <div class="num">03</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 14 L88 46 H78 V86 H22 V46 H12 Z" fill="#FFD9E0"/><rect x="42" y="60" width="16" height="26" fill="#5B3FA8"/></svg>
      </section>
      <section style="background:#2E9E78">
        <div class="num">04</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 10 L74 50 H58 L78 84 H22 L42 50 H26 Z" fill="#E2FFE9"/><rect x="46" y="84" width="8" height="10" fill="#0F1117"/></svg>
      </section>
      <section style="background:#C0453F">
        <div class="num">05</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 8 L61 38 L97 38 L68 60 L79 96 L50 74 L21 96 L32 60 L3 38 L39 38 Z" fill="#FFF3C9"/></svg>
      </section>
    </div>
  </div>
  <div class="lines" data-lines aria-hidden="true"></div>
</div>
CSS
:root{
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-inout:    cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #0F1117; }

.stage {
  position: relative;
  width: 100%;
  height: 100vh;
  min-height: 260px;
  max-height: 100%;
  overflow: hidden;
  background: #0F1117;
  transition: opacity .3s ease;
}
.stage.fading { opacity: 0; }

.track { position: absolute; left: 0; top: 0; width: 100%; will-change: transform; }

/* ブラー/scaleYはWAAPIのtranslateと衝突しないよう内側要素に当てる */
.inner { transition: filter .15s ease, transform .15s ease; }
.stage.dashing .inner { filter: blur(4px); transform: scaleY(1.06); }

section {
  position: relative;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.num {
  font-family: "Arial Black", system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(80px, 22vw, 200px);
  color: rgba(242,242,242,.92);
  line-height: 1;
}
.icon {
  position: absolute;
  width: clamp(60px, 14vw, 120px);
  height: clamp(60px, 14vw, 120px);
  opacity: .9;
}

/* 流線オーバーレイ:ダッシュ中だけ表示。線は常時流れている */
.lines {
  position: fixed; inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .15s ease;
  z-index: 3;
}
.stage.dashing .lines { opacity: 1; }
.line {
  position: absolute;
  top: -160px;
  width: 2px;
  background: rgba(255,255,255,.5);
  animation: streak .3s linear infinite;
}
@keyframes streak {
  from { transform: translateY(0); }
  to   { transform: translateY(100vh); }
}
JavaScript
// フェーズ制御は WAAPI(track.animate)。translate量は視点高さ基準でスケールさせ環境差に強くする。
(() => {
  const stage = document.querySelector('[data-stage]');
  const track = document.querySelector('[data-track]');
  const lines = document.querySelector('[data-lines]');
  if (!stage || !track || !lines) return; // null安全

  // 流線20本を生成(幅2px / 高さ80〜160px / left=(i*137.5)%100% / 位相ずらし)
  for (let i = 0; i < 20; i++) {
    const el = document.createElement('span');
    el.className = 'line';
    el.style.left = ((i * 137.5) % 100) + '%';
    el.style.height = (80 + (i * 53) % 80) + 'px';
    el.style.animationDelay = (-(i % 6) * 0.05) + 's';
    lines.appendChild(el);
  }

  const wait = (ms) => new Promise(r => setTimeout(r, ms));
  const anim = (kf, opts) => track.animate(kf, { fill: 'forwards', ...opts }).finished;

  const ty = (v) => `translateY(${v}px)`;

  async function loop() {
    // ブラウザによっては finished が無いので保険
    if (!track.animate) return;
    while (true) {
      const H = stage.clientHeight || 600;
      const creep = -0.33 * H;
      const dash  = -3.2 * H;
      const over  = dash - 0.12 * H;

      track.style.transform = ty(0);
      stage.classList.remove('fading');

      // 1. 微速前進
      await anim([{ transform: ty(0) }, { transform: ty(creep) }], { duration: 1200, easing: 'linear' });
      // 2. ダッシュ(ブラー+流線+scaleY)
      stage.classList.add('dashing');
      await anim([{ transform: ty(creep) }, { transform: ty(dash) }], { duration: 900, easing: 'cubic-bezier(0.6,0,1,1)' });
      // 3. 着地:オーバーシュート→スプリングで戻る、効果解除
      stage.classList.remove('dashing');
      await anim(
        [{ transform: ty(dash) }, { transform: ty(over), offset: 0.5 }, { transform: ty(dash) }],
        { duration: 600, easing: 'cubic-bezier(0.34,1.56,0.64,1)' });
      // 4. 停止→フェードで先頭へ
      await wait(1000);
      stage.classList.add('fading');
      await wait(300);
    }
  }

  loop();
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「高速スクロール・スピードライン」の効果を追加してください。

# 追加してほしい効果
高速スクロール・スピードライン(スクロール演出)
ゆっくり進んでいたページが突然ヒュンと高速移動し、流線とブラーの残像を残してバウンドしながら停止します。クリスマス特設サイトのような、移動そのものを楽しませるスクロール演出です。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 高速スクロール・スピードライン:突然ヒュンと加速し流線とブラーを残して着地 -->
<div class="stage" data-stage aria-label="高速移動するスクロール演出">
  <div class="track" data-track>
    <div class="inner">
      <section style="background:#0F1117">
        <div class="num">01</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 8 L61 38 L97 38 L68 60 L79 96 L50 74 L21 96 L32 60 L3 38 L39 38 Z" fill="#FFC83D"/></svg>
      </section>
      <section style="background:#2B4DA8">
        <div class="num">02</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M64 12 a40 40 0 1 0 24 70 a32 32 0 1 1 -24 -70 Z" fill="#D9F2FF"/></svg>
      </section>
      <section style="background:#5B3FA8">
        <div class="num">03</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 14 L88 46 H78 V86 H22 V46 H12 Z" fill="#FFD9E0"/><rect x="42" y="60" width="16" height="26" fill="#5B3FA8"/></svg>
      </section>
      <section style="background:#2E9E78">
        <div class="num">04</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 10 L74 50 H58 L78 84 H22 L42 50 H26 Z" fill="#E2FFE9"/><rect x="46" y="84" width="8" height="10" fill="#0F1117"/></svg>
      </section>
      <section style="background:#C0453F">
        <div class="num">05</div>
        <svg class="icon" viewBox="0 0 100 100"><path d="M50 8 L61 38 L97 38 L68 60 L79 96 L50 74 L21 96 L32 60 L3 38 L39 38 Z" fill="#FFF3C9"/></svg>
      </section>
    </div>
  </div>
  <div class="lines" data-lines aria-hidden="true"></div>
</div>

【CSS】
:root{
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-inout:    cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #0F1117; }

.stage {
  position: relative;
  width: 100%;
  height: 100vh;
  min-height: 260px;
  max-height: 100%;
  overflow: hidden;
  background: #0F1117;
  transition: opacity .3s ease;
}
.stage.fading { opacity: 0; }

.track { position: absolute; left: 0; top: 0; width: 100%; will-change: transform; }

/* ブラー/scaleYはWAAPIのtranslateと衝突しないよう内側要素に当てる */
.inner { transition: filter .15s ease, transform .15s ease; }
.stage.dashing .inner { filter: blur(4px); transform: scaleY(1.06); }

section {
  position: relative;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.num {
  font-family: "Arial Black", system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(80px, 22vw, 200px);
  color: rgba(242,242,242,.92);
  line-height: 1;
}
.icon {
  position: absolute;
  width: clamp(60px, 14vw, 120px);
  height: clamp(60px, 14vw, 120px);
  opacity: .9;
}

/* 流線オーバーレイ:ダッシュ中だけ表示。線は常時流れている */
.lines {
  position: fixed; inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .15s ease;
  z-index: 3;
}
.stage.dashing .lines { opacity: 1; }
.line {
  position: absolute;
  top: -160px;
  width: 2px;
  background: rgba(255,255,255,.5);
  animation: streak .3s linear infinite;
}
@keyframes streak {
  from { transform: translateY(0); }
  to   { transform: translateY(100vh); }
}

【JavaScript】
// フェーズ制御は WAAPI(track.animate)。translate量は視点高さ基準でスケールさせ環境差に強くする。
(() => {
  const stage = document.querySelector('[data-stage]');
  const track = document.querySelector('[data-track]');
  const lines = document.querySelector('[data-lines]');
  if (!stage || !track || !lines) return; // null安全

  // 流線20本を生成(幅2px / 高さ80〜160px / left=(i*137.5)%100% / 位相ずらし)
  for (let i = 0; i < 20; i++) {
    const el = document.createElement('span');
    el.className = 'line';
    el.style.left = ((i * 137.5) % 100) + '%';
    el.style.height = (80 + (i * 53) % 80) + 'px';
    el.style.animationDelay = (-(i % 6) * 0.05) + 's';
    lines.appendChild(el);
  }

  const wait = (ms) => new Promise(r => setTimeout(r, ms));
  const anim = (kf, opts) => track.animate(kf, { fill: 'forwards', ...opts }).finished;

  const ty = (v) => `translateY(${v}px)`;

  async function loop() {
    // ブラウザによっては finished が無いので保険
    if (!track.animate) return;
    while (true) {
      const H = stage.clientHeight || 600;
      const creep = -0.33 * H;
      const dash  = -3.2 * H;
      const over  = dash - 0.12 * H;

      track.style.transform = ty(0);
      stage.classList.remove('fading');

      // 1. 微速前進
      await anim([{ transform: ty(0) }, { transform: ty(creep) }], { duration: 1200, easing: 'linear' });
      // 2. ダッシュ(ブラー+流線+scaleY)
      stage.classList.add('dashing');
      await anim([{ transform: ty(creep) }, { transform: ty(dash) }], { duration: 900, easing: 'cubic-bezier(0.6,0,1,1)' });
      // 3. 着地:オーバーシュート→スプリングで戻る、効果解除
      stage.classList.remove('dashing');
      await anim(
        [{ transform: ty(dash) }, { transform: ty(over), offset: 0.5 }, { transform: ty(dash) }],
        { duration: 600, easing: 'cubic-bezier(0.34,1.56,0.64,1)' });
      // 4. 停止→フェードで先頭へ
      await wait(1000);
      stage.classList.add('fading');
      await wait(300);
    }
  }

  loop();
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。