デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ヒーローを画面に固定したまま、スクロールした分だけ画像の枠を縮めていきます。枠は画面の中央に置いたまま、幅と高さを画面いっぱいから 44vw × 56vh のカードへ、角の丸みを 0 から 4px へ変えます。画像の上の見出しは前半で薄くしながら小さくし、縮みきる少し前から、カードの上下に次の節の文言を出します。
縮める動きは scrub でスクロール量にそのまま結び、画面の高さの 1.1 倍のスクロールで縮みきるようにしています。イージングは power1.inOut です。中の画像は 1.08 倍に寄せた所から始め、枠と一緒に等倍へ引くので、カメラが後ろへ下がっていくように見えます。次の節の文言は、進み具合の 65% の所から 24px 下から浮かべます。
縮めるのは transform の scale ではなく、枠の幅と高さです。scale で縮めると角の丸みまで一緒に縮み、画面と違う縦横比のカードにしたときには画像がゆがみます。幅と高さを変え、中の画像に object-fit の cover を付けておけば、どの大きさでも画像はゆがまずに切り抜かれます。
固定は CSS の sticky で作り、セクションの高さを「画面の高さ + 縮めるあいだのスクロール量」にします。この高さが足りないと、縮みきる前に固定が外れて流れていきます。枠の幅・高さ・角丸は、0 から 1 へ動く CSS 変数ひとつから calc() で組んでいるので、狭い画面ではカードの大きさを CSS だけで変えられます。
動きを減らす設定では、縮みきったカードの形で止め、見出しは画像の上に残します。つまみは、カードの大きさ、縮めるあいだのスクロール量、画像の寄りの量です。旅行・建築・ブランドの紹介のように、1 枚の写真でページを始めて、そこから本文へ入っていくヒーローに向いています。
コード
<section class="zoom-hero">
<div class="zoom-hero__stage">
<!-- 縮める枠。中の画像は写真でも動画でもよい -->
<div class="zoom-hero__media">
<img class="zoom-hero__image" src="/img/hero-dusk.jpg" alt="" width="2560" height="1440">
<h1 class="zoom-hero__title">ひらけた場所へ</h1>
<p class="zoom-hero__hint" aria-hidden="true">スクロール ↓</p>
</div>
<!-- 縮みきったカードの上下に出る、次の節の文言 -->
<p class="zoom-hero__eyebrow">Chapter one</p>
<h2 class="zoom-hero__caption">丘に、光が満ちていく。</h2>
</div>
</section>
.zoom-hero {
--ink: #262626;
--mute: #6b6b6b;
--on-image: #ffffff; /* 画像の上の見出し */
--on-image-mute: #dedcd6;
--card-w: 44vw; /* 縮みきったカードの大きさ */
--card-h: 56vh;
--zoom: 110vh; /* 縮みきるまでのスクロール量 */
--p: 0; /* 縮んだ割合(0〜1)。JS がスクロールに合わせて動かす */
position: relative;
height: calc(100svh + var(--zoom));
background-color: #f2f1ed;
color: var(--ink);
}
/* 固定する区間。JS が動かないときも、この固定だけは効く */
.zoom-hero__stage {
position: sticky;
top: 0;
height: 100svh;
overflow: hidden;
}
/* 中央に置いたまま、幅と高さを画面いっぱいからカードの大きさへ縮める。scale と違い、角丸と絵の縦横比が崩れない */
.zoom-hero__media {
position: absolute;
inset: 0;
z-index: 1;
margin: auto;
width: calc(100% + (var(--card-w) - 100%) * var(--p));
height: calc(100% + (var(--card-h) - 100%) * var(--p));
border-radius: calc(4px * var(--p));
overflow: hidden;
}
.zoom-hero__image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.08); /* 少し寄せた所から始め、縮みながら引く */
}
.zoom-hero__title {
position: absolute;
inset: 0;
display: grid;
place-items: center;
margin: 0;
padding: 0 24px;
text-align: center;
color: var(--on-image);
font-size: clamp(44px, 5.6vw, 72px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.02em;
}
.zoom-hero__hint {
position: absolute;
left: 0;
right: 0;
bottom: 40px;
margin: 0;
text-align: center;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--on-image-mute);
}
.zoom-hero__eyebrow,
.zoom-hero__caption {
position: absolute;
left: 0;
right: 0;
margin: 0;
padding: 0 24px;
text-align: center;
}
.zoom-hero__eyebrow { top: 72px; font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
.zoom-hero__caption { bottom: 56px; font-size: clamp(28px, 3.4vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
/* 狭い画面では、カードを縦長寄りにする */
@media (max-width: 640px) {
.zoom-hero { --card-w: 84vw; --card-h: 52vh; }
}
/* 動きを減らす設定では、縮みきったカードの形で止める(JS も縮める動きを作らない) */
@media (prefers-reduced-motion: reduce) {
.zoom-hero { --p: 1; height: 100svh; }
.zoom-hero__image { transform: none; }
.zoom-hero__hint { display: none; }
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".zoom-hero").forEach((hero) => {
if (reduceMotion) return; // 縮みきったカードの形は CSS 側で作る
const next = hero.querySelectorAll(".zoom-hero__eyebrow, .zoom-hero__caption");
// 文字は opacity だけで隠す。visibility で隠すと、読み上げから外れる
gsap.set(next, { opacity: 0, y: 24 });
gsap
.timeline({
// 固定は CSS の sticky。セクションの高さ(画面 + 縮める量)を、そのまま縮める区間にする
scrollTrigger: { trigger: hero, start: "top top", end: "bottom bottom", scrub: true },
})
// 枠の幅・高さ・角丸は、この 1 つの変数から CSS の calc() で決まる
.to(hero, { "--p": 1, duration: 1, ease: "power1.inOut" }, 0)
.to(hero.querySelector(".zoom-hero__image"), { scale: 1, duration: 1, ease: "power1.inOut" }, 0)
.to(hero.querySelector(".zoom-hero__title"), { opacity: 0, scale: 0.8, duration: 0.45, ease: "power1.in" }, 0)
.to(hero.querySelector(".zoom-hero__hint"), { opacity: 0, duration: 0.12, ease: "none" }, 0)
// 次の節の文言は、縮みきる少し前から浮かべる
.to(next, { opacity: 1, y: 0, duration: 0.3, ease: "power2.out", stagger: 0.08 }, 0.65);
});