デモ
デモを読み込めませんでした。
仕様
- 用途
- カード
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
一覧のカードが、行ごとに左から順に、下から少し浮き上がるようにして現れます。隠している間は48px下げて透明にしておき、上端が画面の下から15%の高さに届いたところで、0.8秒の power3.out で元の位置へ戻します。
カードは個別に扱わず、同じころに画面へ入ったものをひとまとめにしました。個別だと、速くスクロールしたときに何枚ものカードがばらばらに動き出し、順に現れる感じがなくなります。まとめたカードは0.1秒ずつ開始をずらすので、3枚並んだ行なら、最後のカードは最初のカードの0.2秒後に動き出します。間隔をこれより広げると、最後のカードを待たされているように感じます。
表示を始める位置(start)を下端より少し内側にしたのは、下端だとカードがのぞいた瞬間に動き始め、いちばん大きく動くところが画面の外で終わってしまうからです。動く距離を大きくしすぎると、読み進める速さに動きが追いつかず、遅れて出てくるように見えます。動きを付けるのは表示する向きだけで、上へ戻したときは何もしないので、一度現れたカードはそのまま残ります。記事の一覧や実績、商品の並びのように、同じ形のカードが何行も続くセクションを想定した作りです。
コード
<section class="cards" aria-labelledby="cards-title">
<h2 class="cards__title" id="cards-title">新着記事</h2>
<div class="cards__grid">
<!-- 同じ形の要素を並べるだけ。枚数が増えても、つなぎ方は変わらない -->
<article class="card">
<a class="card__link" href="/journal/quiet-interfaces/">
<img class="card__image" src="/img/journal-01.jpg" alt="" width="800" height="500">
<h3 class="card__name">静かな画面のつくり方</h3>
<p class="card__text">ここに説明文が入ります。</p>
</a>
</article>
<article class="card">
<a class="card__link" href="/journal/slow-motion/">
<img class="card__image" src="/img/journal-02.jpg" alt="" width="800" height="500">
<h3 class="card__name">ゆっくり動かす理由</h3>
<p class="card__text">ここに説明文が入ります。</p>
</a>
</article>
<article class="card">
<a class="card__link" href="/journal/paper-and-ink/">
<img class="card__image" src="/img/journal-03.jpg" alt="" width="800" height="500">
<h3 class="card__name">紙と墨の余白</h3>
<p class="card__text">ここに説明文が入ります。</p>
</a>
</article>
</div>
</section>
.cards {
--ink: #262626;
--mute: #6b6b6b;
padding: 64px 6vw;
background-color: #f2f1ed;
color: var(--ink);
}
.cards__title {
margin: 0 0 32px;
font-size: clamp(24px, 2.6vw, 32px);
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.02em;
}
.cards__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 32px;
}
/* カードは白い背景。角は 4px で、影は付けない */
.card {
border-radius: 4px;
background-color: #ffffff;
}
.card__link {
display: block;
padding: 16px;
color: inherit;
text-decoration: none;
}
.card__link:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 2px;
}
.card__image {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 10;
object-fit: cover;
}
.card__name {
margin: 16px 0 0;
font-size: 18px;
font-weight: 700;
line-height: 1.4;
letter-spacing: 0.02em;
}
.card__text {
margin: 8px 0 0;
font-size: 14px;
line-height: 1.8;
letter-spacing: 0.04em;
color: var(--mute);
word-break: auto-phrase;
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const cards = gsap.utils.toArray(".card");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 動きを減らす設定では何もしない(カードは最初から表示された状態のまま)
if (cards.length > 0 && !reduceMotion) {
gsap.set(cards, { y: 48, opacity: 0 });
// 同じころに画面へ入ったカードをまとめて扱う。1 枚ずつトリガーを付けると、速く送ったときにばらばらに動き出す
ScrollTrigger.batch(cards, {
// 画面の下端ではなく、少し内側から動かし始める。下端だと、いちばん動く所が画面の外で終わる
start: "top 85%",
onEnter: (batch) =>
gsap.to(batch, { y: 0, opacity: 1, duration: 0.8, ease: "power3.out", stagger: 0.1, overwrite: true }),
});
}