本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ヒーロー

スクロールでヒーローの全画面の画像が縮み、画面の中央のカードに収まる

デモ

操作:スクロール。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。 触っている間は、ホイールでデモの中がスクロールします。

仕様

用途
ヒーロー
動作
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 枚の写真でページを始めて、そこから本文へ入っていくヒーローに向いています。

コード

HTML
<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>

CSS
.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; }
}

JavaScript
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);
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。