デモ
デモを読み込めませんでした。
仕様
- 用途
- セクション
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
左の説明を読み進めるあいだ、右の図は画面の上下中央に留まり続けます。段階の上端が画面の中央を越えるたびに、図がその段階のものへ入れ替わり、いま読んでいる段階の説明だけが濃くなります。ほかの段階は不透明度0.25まで下げ、読める濃さは残しています。
切り替えはスクロール量に比例させるほうが素直に思えますが、そうするとスクロールを止めた位置によっては、図が半分だけ入れ替わった状態で止まります。だから scrub は使わず、線を越えた瞬間から時間で再生します。出ていく図を0.5秒で薄くしながら少し送り出し、0.15秒おいてから、次の図を反対側から0.6秒で入れます。間を置くのは、新旧の図が半透明で重なっている時間を短くしたいからです。
それでも入れ替わりの途中には、どちらの図も半透明になる瞬間があります。図を入れる要素には、ページの背景色とは別の明るい背景色を付けました。付けないとページの背景色が透けて、図が暗く見えます。
sticky には効く範囲があります。固定が効くのは親の要素の中だけなので、図を入れた列を説明の列と同じ高さまで伸ばしておかないと、最初の段階の途中で固定が外れ、図が説明と一緒に流れていきます。段階を増やすときは、ひとつの段階の説明に画面の高さの6割ほどを取ると、次の図へ移る前に読み終えられます。
コード
<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>
.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;
}
}
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),
})
);
});