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

スクロールの速さに合わせてカードの画像が傾く

デモ

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

仕様

用途
カード
動作
DOM
種類
レシピ
使うもの
ScrollTrigger
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

ScrollTrigger の onUpdate でスクロールの速さ(getVelocity、px/秒)を受け取り、画像の傾き(skewY)に変えて渡します。下へ速く流すほど画像の左が下がるように傾き、上へ戻すと逆に傾きます。止まるとまっすぐに戻るので、流しているあいだだけ、紙がしなるような手応えが出ます。傾けるのは画像だけで、題名はまっすぐのまま読めるようにしています。

傾きは、速さ 220px/秒ごとに 1°、上限は 8° です。値は gsap.quickTo に渡し、0.5 秒かけて追いつかせます(power3.out)。スクロールの速さは知らせのたびに細かく揺れるので、そのまま入れると画像が震えます。追いつくまでの時間を置くと揺れがならされ、止まったときも同じ速さでなめらかに戻ります。

急に止めると onUpdate の知らせが来なくなり、画像が傾いたまま残ります。スクロールが止まったときの知らせ(scrollEnd)で 0 に戻します。また、傾いた画像の角は「幅の半分 × tan(角度)」だけ上下にはみ出し(幅 340px・8° で約 24px)、transform を持つ要素は後から描かれるので、下の題名の上に乗ります。画像と題名の間をはみ出しより広くとり、題名に position: relative を付けて画像より上に描きます。

傾きを強くしたいときは、1° あたりの速さを小さくするか、上限を上げます。上限を上げるなら、画像と題名の間も広げてください。動きを減らす設定では傾けません。作品一覧やブログ・記事の一覧、商品のグリッドなど、同じ形のカードが縦に続くページに向いています。

コード

HTML
<section class="works" aria-labelledby="works-title">
  <div class="works__head">
    <div>
      <p class="works__label">Works — 2026</p>
      <h2 class="works__title" id="works-title">制作実績</h2>
    </div>
    <p class="works__lead">ここに説明文が入ります。</p>
  </div>
  <!-- 傾くのは .work__image だけ。題名はまっすぐのまま読めるようにする -->
  <ul class="works__grid">
    <li><a class="work" href="/works/kitakaze-coffee/">
      <img class="work__image" src="/images/work-kitakaze.jpg" alt="" width="800" height="1000">
      <span class="work__title">北風珈琲店</span> <span class="work__meta">ブランディング</span>
    </a></li>
    <li><a class="work" href="/works/shizuku-denki/">
      <img class="work__image" src="/images/work-shizuku.jpg" alt="" width="800" height="1000">
      <span class="work__title">しずく電機</span> <span class="work__meta">プロダクトサイト</span>
    </a></li>
    <li><a class="work" href="/works/komorebi/">
      <img class="work__image" src="/images/work-komorebi.jpg" alt="" width="800" height="1000">
      <span class="work__title">こもれび</span> <span class="work__meta">アプリのUIデザイン</span>
    </a></li>
  </ul>

  <div class="works__head">
    <div>
      <p class="works__label">Archive</p>
      <h3 class="works__title">2025年</h3>
    </div>
    <p class="works__count">3件のプロジェクト</p>
  </div>
  <ul class="works__grid">
    <li><a class="work" href="/works/minato-music/">
      <img class="work__image" src="/images/work-minato.jpg" alt="" width="800" height="1000">
      <span class="work__title">みなと音楽祭</span> <span class="work__meta">広告キャンペーン</span>
    </a></li>
    <li><a class="work" href="/works/hitotsu/">
      <img class="work__image" src="/images/work-hitotsu.jpg" alt="" width="800" height="1000">
      <span class="work__title">季刊ひとつ</span> <span class="work__meta">誌面デザイン</span>
    </a></li>
    <li><a class="work" href="/works/ishi-to-kami/">
      <img class="work__image" src="/images/work-ishi-to-kami.jpg" alt="" width="800" height="1000">
      <span class="work__title">石と紙展</span> <span class="work__meta">展示デザイン</span>
    </a></li>
  </ul>

  <div class="works__head">
    <div>
      <p class="works__label">Archive</p>
      <h3 class="works__title">2024年</h3>
    </div>
    <p class="works__count">3件のプロジェクト</p>
  </div>
  <ul class="works__grid">
    <li><a class="work" href="/works/yamanocha/">
      <img class="work__image" src="/images/work-yamanocha.jpg" alt="" width="800" height="1000">
      <span class="work__title">山ノ茶</span> <span class="work__meta">パッケージデザイン</span>
    </a></li>
    <li><a class="work" href="/works/tsunagu/">
      <img class="work__image" src="/images/work-tsunagu.jpg" alt="" width="800" height="1000">
      <span class="work__title">つなぐ会議</span> <span class="work__meta">カンファレンス</span>
    </a></li>
    <li><a class="work" href="/works/yasuragi/">
      <img class="work__image" src="/images/work-yasuragi.jpg" alt="" width="800" height="1000">
      <span class="work__title">やすらぎ</span> <span class="work__meta">ヘルスケアアプリ</span>
    </a></li>
  </ul>

  <div class="works__head">
    <div>
      <p class="works__label">Archive</p>
      <h3 class="works__title">2023年以前</h3>
    </div>
    <a class="works__more" href="/works/archive/">過去の作品をすべて見る →</a>
  </div>
</section>

CSS
.works {
  --bg: #111111;
  --ink: #eeece6;
  --mute: #898989;
  --mute-small: #a8a6a0;
  --holder: #262626; /* 画像が読み込まれるまでの地 */
  padding: 0 clamp(16px, 7.5vw, 96px) 96px;
  background: var(--bg);
  color: var(--ink);
}

/* 見出しの区間。上下に広く間をとり、画像の行と交互に並べる */
.works__head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 24px 64px;
  padding-block: clamp(96px, 24vh, 240px);
}

.works__label {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mute-small);
}

.works__title {
  margin: 16px 0 0;
  font-size: clamp(40px, 6vw, 72px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.works__lead { margin: 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }
.works__count { margin: 0; font-size: 18px; letter-spacing: 0.04em; color: var(--mute); font-variant-numeric: tabular-nums; }

.works a { color: inherit; }
.works a:focus-visible { outline: 2px solid var(--ink); outline-offset: 8px; }

.works__more {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.4em;
}

/* 3 列。真ん中の列だけ下げて段差をつける */
.works__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
  gap: 48px 32px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.works__grid > :nth-child(3n + 2) { margin-top: 48px; }

/* 画像と題名の間(32px)は、傾いた角のはみ出し(幅の半分 × tan 8° ≒ 24px)より広くとる */
.work {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: last baseline;
  gap: 32px 16px;
  text-decoration: none;
}

.work__image {
  grid-column: 1 / -1;
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 4px;
  background: var(--holder);
}

/* transform を持つ画像は後から描かれるので、位置を持たせて題名を画像より上に描く */
.work__title,
.work__meta { position: relative; }

.work__title { font-size: 24px; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em; }
.work__meta { font-size: 14px; letter-spacing: 0.04em; color: var(--mute-small); white-space: nowrap; }

@media (max-width: 767px) {
  .works__grid { grid-template-columns: minmax(0, 1fr); }
  .works__grid > :nth-child(3n + 2) { margin-top: 0; }
}

JavaScript
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const MAX_SKEW = 8; // 傾きの上限(deg)。上げるなら、画像と題名の間も広げる
const PER_DEGREE = 220; // スクロールの速さ(px/秒)がこれだけ増えるごとに 1° 傾ける

// 動きを減らす設定では傾けない(設定を切り替えると付け外しされる)
gsap.matchMedia().add("(prefers-reduced-motion: no-preference)", () => {
  const images = gsap.utils.toArray(".work__image");
  if (images.length === 0) return;
  // 傾きは 0.5 秒かけて追いつかせる。速さの細かい揺れがならされ、止まるとなめらかに戻る
  const skewTo = gsap.quickTo(images, "skewY", { duration: 0.5, ease: "power3.out" });
  const clamp = gsap.utils.clamp(-MAX_SKEW, MAX_SKEW);

  ScrollTrigger.create({
    // getVelocity() は px/秒。下へ流すと負の角度(画像の左が下がる)、上へ戻すと逆に傾く
    onUpdate: (self) => skewTo(clamp(-self.getVelocity() / PER_DEGREE)),
  });

  // 急に止まったときは onUpdate が来ないので、スクロールが止まったらまっすぐに戻す
  const settle = () => skewTo(0);
  ScrollTrigger.addEventListener("scrollEnd", settle);
  return () => ScrollTrigger.removeEventListener("scrollEnd", settle);
});

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

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