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

ホバーでカードの画像が寄り、キャプションが下から上がる

デモ

操作:カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

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

しくみと調整

カーソルが重なったら、overflow: hidden の枠の中で画像を 1.14 倍へゆっくり寄せ、下半分に暗いグラデーションを重ねて、分類と作品名を行の下から持ち上げます。続けて右下の矢印の丸を、回しながら大きくして出します。動きは止めたタイムライン 1 本にまとめ、重なったら play()、離れたら reverse() します。

画像の寄りは 0.9 秒、キャプションの行は 0.6 秒で 0.06 秒ずつずらし、どれも ease は power2.inOut です。out 系の ease や行き過ぎる back.out を逆にたどると、戻り始めがゆっくりになり、隣の作品へ移ったあとも前のキャプションが残って、2 枚とも出て見えます。離れたときは timeScale で入るときの倍の速さにして戻します。

キャプションの各行は枠で包んではみ出しを隠し、中身を yPercent 110 から 0 へ動かします。沈めた初めの位置を CSS の transform に書くと、GSAP がそれを px として読み込み、yPercent と二重になって出てこなくなります。初めの位置は、タイムラインの fromTo に置かせます。

カード全体を 1 つのリンクにし、名前は作品名だけ、分類は説明として読ませます。キーボードで焦点が入ったときも同じ動きにし、動きを減らす設定ではほぼ一瞬で切り替えます。ホバーの無いタッチ端末では、キャプションをはじめから見せたままにします。

寄りを 1.05 前後にすると上品に、大きくすると派手になります。暗いグラデーションは文字を読ませるためのものなので、明るい画像ほど濃くします。ポートフォリオの一覧・商品のグリッド・記事カードに向いています。

コード

HTML
<section class="works" aria-labelledby="works-title">
  <div class="works__head">
    <h2 class="works__title" id="works-title">最近の制作実績</h2>
    <a class="works__all" href="/works/">すべての作品を見る →</a>
  </div>

  <ul class="works__grid">
    <li>
      <!-- カード全体が 1 つのリンク。名前は作品名だけにし、分類は説明として読ませる -->
      <a class="work" href="/works/seaside-house/" aria-labelledby="work-1-name" aria-describedby="work-1-kind">
        <img class="work__image" src="/img/seaside-house.jpg" alt="" width="704" height="896">
        <span class="work__shade"></span>
        <span class="work__index" aria-hidden="true">01</span>
        <span class="work__caption">
          <!-- 行ごとに overflow: hidden の枠で包み、中身を行の下から持ち上げる -->
          <span class="work__line"><span class="work__kind" id="work-1-kind">インテリア</span></span>
          <span class="work__line"><span class="work__name" id="work-1-name">海辺の平屋</span></span>
          <span class="work__button" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M7 17 17 7M9 7h8v8" /></svg></span>
        </span>
      </a>
    </li>

    <li>
      <a class="work" href="/works/night-garden/" aria-labelledby="work-2-name" aria-describedby="work-2-kind">
        <img class="work__image" src="/img/night-garden.jpg" alt="" width="704" height="896">
        <span class="work__shade"></span>
        <span class="work__index" aria-hidden="true">02</span>
        <span class="work__caption">
          <span class="work__line"><span class="work__kind" id="work-2-kind">エディトリアル</span></span>
          <span class="work__line"><span class="work__name" id="work-2-name">夜の庭</span></span>
          <span class="work__button" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M7 17 17 7M9 7h8v8" /></svg></span>
        </span>
      </a>
    </li>

    <li>
      <!-- 明るい画像の上は、番号を墨色にする -->
      <a class="work work--light" href="/works/paper-suns/" aria-labelledby="work-3-name" aria-describedby="work-3-kind">
        <img class="work__image" src="/img/paper-suns.jpg" alt="" width="704" height="896">
        <span class="work__shade"></span>
        <span class="work__index" aria-hidden="true">03</span>
        <span class="work__caption">
          <span class="work__line"><span class="work__kind" id="work-3-kind">展覧会</span></span>
          <span class="work__line"><span class="work__name" id="work-3-name">紙の太陽</span></span>
          <span class="work__button" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M7 17 17 7M9 7h8v8" /></svg></span>
        </span>
      </a>
    </li>
  </ul>
</section>

CSS
.works {
  --ground: #111111;        /* 地 */
  --ink: #eeece6;           /* 見出しとリンク */
  --on-image: #ffffff;      /* 暗いグラデーションの上の題 */
  --on-image-mute: #dedcd6; /* 同じく分類のラベル */
  --on-light: #262626;      /* 明るい画像の上の番号 */
  --button: #ffffff;        /* 矢印の丸 */
  --button-ink: #111111;    /* 丸の上の矢印 */
  --zoom: 1.14;             /* ホバー中の画像の寄り */
  padding: 64px clamp(16px, 6vw, 96px);
  background: var(--ground);
  color: var(--ink);
}

.works__head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: last baseline;
  gap: 16px 32px;
  margin-bottom: 48px;
}

.works__title { margin: 0; font-size: clamp(32px, 5vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
.works__all { color: inherit; font-size: 15px; font-weight: 600; letter-spacing: 0.04em; text-decoration-thickness: 1px; text-underline-offset: 0.4em; }

.works__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 32px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* 画像はこの枠の中で寄るので、枠の大きさは変わらない */
.work {
  position: relative;
  display: block;
  aspect-ratio: 11 / 14;
  overflow: hidden;
  border-radius: 4px;
  background: #2a2a2a;
  color: var(--on-image);
  text-decoration: none;
}

.work:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.work__image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* キャプションを読ませる暗いグラデーション。明るい画像ほど濃くする */
.work__shade { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(17, 17, 17, 0) 40%, rgba(17, 17, 17, 0.78) 100%); }

.work__index {
  position: absolute;
  left: 24px;
  top: 24px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
}

.work--light .work__index { color: var(--on-light); }

.work__caption {
  position: absolute;
  left: 24px;
  right: 24px;
  bottom: 24px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  column-gap: 16px;
}

/* 行の枠。中身は GSAP で行の下へ沈めておく(初めの位置を CSS の transform に書くと、GSAP が px として読んで二重になる) */
.work__line { display: block; grid-column: 1; overflow: hidden; }
.work__line:first-child { margin-bottom: 8px; }
.work__line > span { display: block; }

.work__kind { color: var(--on-image-mute); font-size: 13px; font-weight: 700; line-height: 1.2; letter-spacing: 0.06em; }
.work__name { font-size: 24px; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em; }

.work__button {
  display: grid;
  grid-column: 2;
  grid-row: 1 / span 2;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--button);
  color: var(--button-ink);
}

.work__button svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

JavaScript
import { gsap } from "gsap";

const LEAVE = 2; // 離れたときに戻る速さ(入るときの倍率)。隣へ移ったとき、前のキャプションが残って 2 枚とも出て見えない
// 指で触る端末ではホバーが無いので、キャプションをはじめから見せたままにする(CSS の初めの見た目のまま)
const canHover = window.matchMedia("(hover: hover)").matches;
// 動きを減らす設定では、ほぼ一瞬で切り替える
const speed = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 20 : 1;

if (canHover) {
  document.querySelectorAll(".work").forEach((work) => {
    const zoom = parseFloat(getComputedStyle(work).getPropertyValue("--zoom"));

    // どれも行きと帰りで同じ形になる inOut の ease にする。out や back.out を逆にたどると、
    // 戻り始めがゆっくりになり(back.out はいったん跳ねてから沈む)、隣へ移っても前のキャプションが残る
    const hover = gsap
      .timeline({ paused: true })
      .fromTo(work.querySelector(".work__image"), { scale: 1 }, { scale: zoom, duration: 0.9, ease: "power2.inOut" }, 0)
      .fromTo(work.querySelector(".work__shade"), { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.5, ease: "power2.inOut" }, 0)
      .fromTo(work.querySelectorAll(".work__line > span"), { yPercent: 110 }, { yPercent: 0, duration: 0.6, ease: "power2.inOut", stagger: 0.06 }, 0.05)
      .fromTo(work.querySelector(".work__button"), { scale: 0, rotation: -45 }, { scale: 1, rotation: 0, duration: 0.5, ease: "power2.inOut" }, 0.2);

    const show = () => hover.timeScale(speed).play();
    const hide = () => {
      // カーソルもキーボードの焦点もカードから外れたときだけ戻す
      if (work.matches(":hover") || work.matches(":focus-visible")) return;
      hover.timeScale(speed * LEAVE).reverse();
    };

    work.addEventListener("pointerenter", show);
    work.addEventListener("pointerleave", hide);
    // キーボードで送ったときも、ホバーと同じようにキャプションを出す
    work.addEventListener("focus", show);
    work.addEventListener("blur", hide);
  });
}

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

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