デモ
デモを読み込めませんでした。
仕様
- 用途
- セクション
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
右の図を sticky で画面の上下中央に固定したまま、左の説明だけがスクロールで流れていきます。各段階の見出しが画面の中央を越えるたびに、右の図がその段階の図へ入れ替わり、読んでいる段階の説明だけが濃くなります。
入れ替えは、出ていく図を 0.5 秒で薄くして少し送り出し、0.15 秒おいてから次の図を反対側から 0.6 秒で入れます。間を空けるのは、2 枚が同時に半透明で重なる時間を短くするためです。読んでいない段階の説明は薄さ 0.25 まで下げ、読める濃さを残したまま後ろへ回します。
スクロール量にそのまま比例させる scrub は使わず、線を越えた瞬間から時間で再生します。比例させると、スクロールを止めたところで図が半分だけ入れ替わった状態のまま止まってしまうためです。
sticky が効くのは親の要素の中だけです。図を入れた列が、説明の列と同じ高さまで伸びるように組んでおかないと、図は最初の段階の途中で固定が外れ、説明と一緒に流れていきます。図の後ろには地とは別の明るい面を敷いておくと、入れ替わりの途中で 2 枚とも半透明になったときにも、図が沈んで見えません。
段階を増やすときは、1 段階ぶんの説明の高さを画面の 6 割ほど取ると、次の図に移る前に説明を読み終えられます。制作の流れ、料金プランの比較、機能の紹介など、順を追って見せたい節に向いています。
コード
<section class="steps">
<div class="steps__intro">
<p class="steps__eyebrow">How it works</p>
<h2 class="steps__heading">Three steps to a calmer launch.</h2>
</div>
<div class="steps__body">
<ol class="steps__list">
<li class="step">
<p class="step__number">01</p>
<h3 class="step__name">Gather</h3>
<p class="step__text">資料や素材、メモをひとつのボードに集めて、古い版のまま作業する人をなくします。</p>
</li>
<li class="step">
<p class="step__number">02</p>
<h3 class="step__name">Shape</h3>
<p class="step__text">流れを描き、本物の原稿で試して、役に立たないものは思いきって削ります。</p>
</li>
<li class="step">
<p class="step__number">03</p>
<h3 class="step__name">Launch</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>
.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,
.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;
}
.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;
}
}
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),
})
);
});