デモ
デモを読み込めませんでした。
仕様
- 用途
- カード
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
カードを同じ親の直下に並べ、sticky で画面の上に止めます。止まる位置を 1 枚ごとに 24px ずつ下げているので、あとから来たカードが前のカードの上に重なり、下になったカードの上の縁が帯のように残ります。重なったカードは、自分が止まってから最後のカードが止まるまでのあいだに、奥へ縮みながら暗くなります。
1 枚下になるごとに、大きさを 5% ずつ縮め、墨色の幕の濃さを 0.12 ずつ上げます。4 枚なら、いちばん下のカードは 0.85 倍まで縮み、幕は 0.36 まで濃くなります。縮める起点は上の辺の中央(transform-origin を 50% 0)にしているので、縮んでも上の縁の位置が変わらず、重なった帯の幅がそろったまま残ります。縮みと幕はスクロール量にそのまま結ぶ(scrub)ので、指を止めたところで止まります。
縮めるのはカードの中身で、止めている外枠は動かしません。また position: sticky はその親の中でしか効かないので、カードを 1 枚ずつ別の箱に入れると、次のカードが来る前に固定が外れて流れていきます。止まったカードは、測ると止まった位置を返します。縮み始める位置は、動かないリストの上端から何枚分下かで数えます。
つまみは、止まる位置のずれ、縮む量と暗くなる量、カードの高さです。ずれを広げると重なった帯が太くなって何枚あるか読みやすくなりますが、そのぶん最後のカードの下が画面からはみ出しやすくなります。写真をカードの半分に端まで敷いておくと、帯にそれぞれの写真の色がのぞきます。動きを減らす設定では縮めず、重なるだけにします。会社の考え方・サービスの特長・制作の手順など、数枚のカードを順に読ませたい節に向いています。
コード
<section class="stack" aria-labelledby="stack-title">
<div class="stack__intro">
<p class="stack__eyebrow">Principles</p>
<h2 class="stack__title" id="stack-title">大切にしていること</h2>
</div>
<!-- カードは同じ親の直下に並べる。sticky が効くのはこの親の中だけなので、最後まで全部が固定されたまま重なる。
--i は何枚目か(止まる位置を 1 枚ごとに下へずらす) -->
<ol class="stack__list">
<li class="stack-card" style="--i: 0">
<div class="stack-card__inner">
<div class="stack-card__copy">
<p class="stack-card__number" aria-hidden="true">01</p>
<h3 class="stack-card__title">伝わりやすさを第一に</h3>
<p class="stack-card__text">ここに説明文が入ります。</p>
</div>
<img class="stack-card__image" src="/img/principle-clarity.jpg" alt="" width="1024" height="1040">
</div>
</li>
<li class="stack-card" style="--i: 1">
<div class="stack-card__inner">
<div class="stack-card__copy">
<p class="stack-card__number" aria-hidden="true">02</p>
<h3 class="stack-card__title">表示の速さにこだわる</h3>
<p class="stack-card__text">ここに説明文が入ります。</p>
</div>
<img class="stack-card__image" src="/img/principle-fast.jpg" alt="" width="1024" height="1040">
</div>
</li>
<li class="stack-card" style="--i: 2">
<div class="stack-card__inner">
<div class="stack-card__copy">
<p class="stack-card__number" aria-hidden="true">03</p>
<h3 class="stack-card__title">長く使える設計に</h3>
<p class="stack-card__text">ここに説明文が入ります。</p>
</div>
<img class="stack-card__image" src="/img/principle-last.jpg" alt="" width="1024" height="1040">
</div>
</li>
<li class="stack-card" style="--i: 3">
<div class="stack-card__inner">
<div class="stack-card__copy">
<p class="stack-card__number" aria-hidden="true">04</p>
<h3 class="stack-card__title">使う人の声を聞く</h3>
<p class="stack-card__text">ここに説明文が入ります。</p>
</div>
<img class="stack-card__image" src="/img/principle-people.jpg" alt="" width="1024" height="1040">
</div>
</li>
</ol>
</section>
.stack {
--bg: #f2f1ed;
--surface: #ffffff;
--ink: #262626;
--mute: #6b6b6b;
--top: 72px; /* 1 枚目が止まる位置 */
--offset: 24px; /* 2 枚目からは、この分ずつ下にずれて止まる(重なった縁が見える) */
--gap: 72px; /* 止まる前のカードの間 */
--card-height: min(520px, 100vh - 200px);
--width: min(80%, 1024px);
padding: 96px 0 96px;
background: var(--bg);
color: var(--ink);
}
.stack__intro { width: var(--width); margin: 0 auto 40px; }
.stack__eyebrow,
.stack-card__number {
margin: 0;
font-size: 13px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.14em;
color: var(--mute);
}
.stack__eyebrow { margin-bottom: 16px; text-transform: uppercase; }
.stack__title,
.stack-card__title {
margin: 0;
font-size: clamp(28px, 4vw, 40px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
word-break: auto-phrase;
}
.stack__list { margin: 0; padding: 0; list-style: none; }
/* 最後のカードが止まってから、少しだけ重ねたまま流す(sticky の範囲に含める) */
.stack__list::after { content: ""; display: block; height: 72px; }
.stack-card {
position: sticky;
top: calc(var(--top) + var(--i) * var(--offset));
width: var(--width);
height: var(--card-height);
margin: 0 auto var(--gap);
}
.stack-card:last-child { margin-bottom: 0; }
/* 縮めるのは中身。sticky の外枠を動かすと、止まる位置の計算がずれる */
.stack-card__inner {
--shade: 0;
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
overflow: hidden;
border-radius: 4px;
background: var(--surface);
transform-origin: 50% 0;
}
/* 下になったカードを暗くする幕。濃さはスクリプトが --shade で上げる */
.stack-card__inner::after {
content: "";
position: absolute;
inset: 0;
background: var(--ink);
opacity: var(--shade);
pointer-events: none;
}
.stack-card__copy { display: flex; flex-direction: column; padding: 48px; }
.stack-card__number { margin-bottom: auto; font-variant-numeric: tabular-nums; }
.stack-card__text {
margin: 24px 0 0;
font-size: 18px;
line-height: 1.8;
letter-spacing: 0.04em;
word-break: auto-phrase;
}
/* 写真はカードの端まで敷く。重なった縁に、それぞれの写真の色がのぞく */
.stack-card__image { width: 100%; height: 100%; object-fit: cover; }
/* 狭い画面では写真を上に置き、文を下に回す */
@media (max-width: 700px) {
.stack { --width: calc(100% - 32px); --gap: 48px; }
.stack-card__inner { grid-template-columns: 1fr; grid-template-rows: 38% 1fr; }
.stack-card__image { order: -1; }
.stack-card__copy { padding: 24px; }
.stack-card__text { margin-top: 16px; font-size: 15px; }
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const DEPTH_SCALE = 0.05; // 1 枚下になるごとに縮む量
const DEPTH_SHADE = 0.12; // 1 枚下になるごとに暗くなる量
function stackCards(list) {
const cards = gsap.utils.toArray(".stack-card", list);
const last = cards.length - 1;
if (last < 1) return;
// sticky のカードは、止まると測った位置が変わる。位置は動かない親(リスト)から数える
const step = () => cards[0].offsetHeight + parseFloat(getComputedStyle(cards[0]).marginBottom);
// i 枚目が止まるスクロール位置 = リストの上端から i 枚分下が、そのカードの top の位置に来たとき
const stickAt = (i) => () => `top+=${i * step()} ${getComputedStyle(cards[i]).top}`;
// i 枚目は、自分が止まってから最後のカードが止まるまでの間に、奥へ縮んで暗くなる
cards.slice(0, last).forEach((card, i) => {
const depth = last - i; // 最後に何枚の下になるか
gsap.to(card.querySelector(".stack-card__inner"), {
scale: 1 - depth * DEPTH_SCALE,
"--shade": depth * DEPTH_SHADE,
ease: "none", // スクロール量にそのまま比例させる
scrollTrigger: {
trigger: list,
start: stickAt(i),
end: stickAt(last),
scrub: true, // 数値にすると、指を止めたあとも遅れて縮む
},
});
});
}
// 動きを減らす設定では縮めず、重なるだけにする(設定を切り替えると付け外しされる)
gsap.matchMedia().add("(prefers-reduced-motion: no-preference)", () => {
document.querySelectorAll(".stack__list").forEach(stackCards);
});