本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

セクション

スクロールでセクションの図が固定され、段階ごとに切り替わる

デモ

操作:スクロール。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。 触っている間は、ホイールでデモの中がスクロールします。

仕様

動作
DOM
種類
レシピ
使うもの
ScrollTrigger
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

左の説明を読み進めるあいだ、右の図は画面の上下中央に留まり続けます。段階の上端が画面の中央を越えるたびに、図がその段階のものへ入れ替わり、いま読んでいる段階の説明だけが濃くなります。ほかの段階は不透明度0.25まで下げ、読める濃さは残しています。

切り替えはスクロール量に比例させるほうが素直に思えますが、そうするとスクロールを止めた位置によっては、図が半分だけ入れ替わった状態で止まります。だから scrub は使わず、線を越えた瞬間から時間で再生します。出ていく図を0.5秒で薄くしながら少し送り出し、0.15秒おいてから、次の図を反対側から0.6秒で入れます。間を置くのは、新旧の図が半透明で重なっている時間を短くしたいからです。

それでも入れ替わりの途中には、どちらの図も半透明になる瞬間があります。図を入れる要素には、ページの背景色とは別の明るい背景色を付けました。付けないとページの背景色が透けて、図が暗く見えます。

sticky には効く範囲があります。固定が効くのは親の要素の中だけなので、図を入れた列を説明の列と同じ高さまで伸ばしておかないと、最初の段階の途中で固定が外れ、図が説明と一緒に流れていきます。段階を増やすときは、ひとつの段階の説明に画面の高さの6割ほどを取ると、次の図へ移る前に読み終えられます。

コード

HTML
<section class="steps">
  <div class="steps__intro">
    <p class="steps__eyebrow">Process</p>
    <h2 class="steps__heading">公開までの、<wbr>3つのステップ。</h2>
  </div>

  <div class="steps__body">
    <ol class="steps__list">
      <li class="step">
        <p class="step__number">01</p>
        <h3 class="step__name">集める</h3>
        <p class="step__text">ここに説明文が入ります。</p>
      </li>
      <li class="step">
        <p class="step__number">02</p>
        <h3 class="step__name">形にする</h3>
        <p class="step__text">ここに説明文が入ります。</p>
      </li>
      <li class="step">
        <p class="step__number">03</p>
        <h3 class="step__name">公開する</h3>
        <p class="step__text">ここに説明文が入ります。</p>
      </li>
    </ol>

    <!-- 図を包む列。説明の列と同じ高さに伸びる(sticky が効くのは、この親の中だけ) -->
    <div class="steps__aside">
      <figure class="steps__figure">
        <img class="steps__slide" src="/img/step-gather.jpg" alt="ボードに集めた資料と付箋" width="1200" height="800">
        <img class="steps__slide" src="/img/step-shape.jpg" alt="画面の流れを描いた下書き" width="1200" height="800">
        <img class="steps__slide" src="/img/step-launch.jpg" alt="公開したあとの利用状況のグラフ" width="1200" height="800">
      </figure>
    </div>
  </div>
</section>

CSS
.steps {
  --ink: #eeece6;
  --mute: #a8a6a0;
  padding: 0 6vw;
  background-color: #111111;
  color: var(--ink);
}

.steps__intro {
  padding: 24vh 0 6vh;
}

.steps__eyebrow,
.step__number {
  margin: 0 0 16px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.14em;
  color: var(--mute);
}

.steps__eyebrow {
  text-transform: uppercase;
}

.steps__heading {
  word-break: keep-all; /* 狭い画面では、<wbr> の所(読点の後)で折り返す */
}

.steps__heading,
.step__name {
  margin: 0;
  font-size: clamp(28px, 3.4vw, 44px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.steps__body {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  column-gap: 80px;
}

.steps__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* 1 段階ぶんの高さ。次の図へ移る前に読み終えられる長さにする */
.step {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 62vh;
}

.step__text {
  margin: 16px 0 0;
  font-size: 18px;
  line-height: 1.8;
  letter-spacing: 0.04em;
  word-break: auto-phrase; /* 和文を語の途中で折り返さず、文節の切れ目で折り返す */
}

.steps__figure {
  position: sticky;
  top: 18vh;
  height: 64vh;
  margin: 0;
  border-radius: 4px;
  overflow: hidden;
  /* 入れ替わりの途中で 2 枚とも半透明になっても、図が暗く見えないように明るい背景色を下に置く */
  background-color: #f2f1ed;
}

.steps__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 列が並ばない幅では固定をやめ、図を段階ごとに置いて読ませる */
@media (max-width: 800px) {
  .steps__body {
    grid-template-columns: 1fr;
  }

  .steps__figure {
    position: static;
    height: 46vh;
    margin-bottom: 6vh;
  }
}

JavaScript
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const DIM = 0.25; // 読んでいない段階の説明の濃さ
// 動きを減らす設定では、切り替えそのものは残したまま、ほぼ一瞬で入れ替える
const SPEED = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0.01 : 1;

document.querySelectorAll(".steps").forEach((section) => {
  const steps = gsap.utils.toArray(".step", section);
  const slides = gsap.utils.toArray(".steps__slide", section);
  let current = 0;

  gsap.set(slides.slice(1), { autoAlpha: 0 });
  gsap.set(steps.slice(1), { opacity: DIM });

  const show = (next) => {
    if (next === current) return;
    const dir = next > current ? 1 : -1; // 上へ戻すときは、出入りの向きも逆にする
    gsap.to(slides[current], { autoAlpha: 0, y: -40 * dir, duration: 0.5 * SPEED, ease: "power2.in", overwrite: true });
    // 0.15 秒おいてから入れて、2 枚が同時に半透明で重なる時間を短くする
    gsap.fromTo(
      slides[next],
      { autoAlpha: 0, y: 40 * dir },
      { autoAlpha: 1, y: 0, duration: 0.6 * SPEED, delay: 0.15 * SPEED, ease: "power2.out", overwrite: true }
    );
    gsap.to(steps, { opacity: (i) => (i === next ? 1 : DIM), duration: 0.4 * SPEED, overwrite: true });
    current = next;
  };

  // 段階の上端が画面の中央を越えたら、その段階をアクティブにする(スクロール量には比例させない)
  steps.forEach((step, i) =>
    ScrollTrigger.create({
      trigger: step,
      start: "top center",
      end: "bottom center",
      onToggle: (self) => self.isActive && show(i),
    })
  );
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。