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

ホバーでメンバーカードの写真が白黒から色づき、SNS のアイコンが順に出る

デモ

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

仕様

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

しくみと調整

写真に filter で白黒と暗さをかけておき、カーソルが重なったら元の色と明るさへ補間します。同時に、写真の枠の下端の外に隠しておいた SNS のアイコンの丸を、0.06 秒ずつずらして下からせり上げます。動きは止めたタイムライン 1 本にまとめ、重なったら play()、離れたら reverse() します。

白黒にするだけでは明るさが変わらないので、休みの写真は明るさを 72% に落としておきます。こうすると、ホバーしたメンバーだけが暗い列から浮かび上がって見えます。filter の値は、休みとホバーで関数の数と並び(grayscale → brightness)をそろえます。そろっていれば、GSAP が数ごとに補間します。アイコンは 0.45 秒の back.out で、止まる前に少し行き過ぎさせています。

色づきの ease を out 系にすると、逆にたどったときに出だしがゆっくりになり、隣のカードへ移ったあとも前の人の色がしばらく残って、2 人とも色づいて見えます。行きと帰りで同じ形になる sine.inOut にし、離れたときは timeScale で入るときの倍の速さにして戻します。

アイコンは隠れているあいだも Tab で届くリンクのままにし、焦点がカードの中へ入ったら(focusin)同じ動きで出します。ホバーの無いタッチ端末では、はじめから色とアイコンを見せます。アイコンは汎用の線の形にしているので、案件では各サービスのロゴに差し替えます。

カードを裏返して紹介文を見せる形と違い、写真の色とアイコンだけで反応を返すので、一覧の並びも高さも変わりません。明るさを 0.5 近くまで下げると、ホバーした人がより強く浮かびます。アイコンのずらしを 0.1 秒にすると、1 つずつ出る感じがはっきりします。会社案内・チーム紹介・登壇者の一覧に向いています。

コード

HTML
<section class="team" aria-labelledby="team-title">
  <p class="team__label">OUR TEAM</p>
  <h2 class="team__title" id="team-title">メンバー紹介</h2>

  <!-- アイコンの形は 1 か所に書き、use で使い回す(案件では各サービスのロゴに差し替える) -->
  <svg width="0" height="0" aria-hidden="true" style="position: absolute">
    <symbol id="team-icon-chat" viewBox="0 0 24 24"><path d="M5 5h14v10h-8l-4 4v-4H5z" /></symbol>
    <symbol id="team-icon-camera" viewBox="0 0 24 24"><rect x="3" y="7" width="18" height="13" rx="3" /><circle cx="12" cy="13.5" r="3.5" /><path d="M8.5 7 10 4h4l1.5 3" /></symbol>
    <symbol id="team-icon-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3c3 3.2 3 14.8 0 18M12 3c-3 3.2-3 14.8 0 18" /></symbol>
    <symbol id="team-icon-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2.5" /><path d="m4 7 8 6 8-6" /></symbol>
  </svg>

  <ul class="team__grid">
    <!-- --tint は写真が届くまでの地の色 -->
    <li class="member" style="--tint: #d99590">
      <div class="member__photo">
        <img src="/img/team-takahashi.jpg" alt="" width="496" height="704">
        <!-- アイコンは隠れていても Tab で届く。焦点が入ったら、ホバーと同じように出す -->
        <ul class="member__social" aria-label="高橋 奈央さんのSNSと連絡先">
          <li><a href="https://example.com/@takahashi" aria-label="SNS"><svg aria-hidden="true"><use href="#team-icon-chat" /></svg></a></li>
          <li><a href="https://example.com/takahashi/photos" aria-label="写真"><svg aria-hidden="true"><use href="#team-icon-camera" /></svg></a></li>
          <li><a href="https://example.com/takahashi" aria-label="ウェブサイト"><svg aria-hidden="true"><use href="#team-icon-globe" /></svg></a></li>
          <li><a href="mailto:sample@higashiyuki.com" aria-label="メール"><svg aria-hidden="true"><use href="#team-icon-mail" /></svg></a></li>
        </ul>
      </div>
      <h3 class="member__name">高橋 奈央</h3>
      <p class="member__role">アートディレクター</p>
    </li>

    <li class="member" style="--tint: #93b2c9">
      <div class="member__photo">
        <img src="/img/team-mori.jpg" alt="" width="496" height="704">
        <ul class="member__social" aria-label="森 海斗さんのSNSと連絡先">
          <li><a href="https://example.com/@mori" aria-label="SNS"><svg aria-hidden="true"><use href="#team-icon-chat" /></svg></a></li>
          <li><a href="https://example.com/mori/photos" aria-label="写真"><svg aria-hidden="true"><use href="#team-icon-camera" /></svg></a></li>
          <li><a href="https://example.com/mori" aria-label="ウェブサイト"><svg aria-hidden="true"><use href="#team-icon-globe" /></svg></a></li>
          <li><a href="mailto:sample@higashiyuki.com" aria-label="メール"><svg aria-hidden="true"><use href="#team-icon-mail" /></svg></a></li>
        </ul>
      </div>
      <h3 class="member__name">森 海斗</h3>
      <p class="member__role">UIデザイナー</p>
    </li>

    <li class="member" style="--tint: #a2c49f">
      <div class="member__photo">
        <img src="/img/team-ishida.jpg" alt="" width="496" height="704">
        <ul class="member__social" aria-label="石田 恵理さんのSNSと連絡先">
          <li><a href="https://example.com/@ishida" aria-label="SNS"><svg aria-hidden="true"><use href="#team-icon-chat" /></svg></a></li>
          <li><a href="https://example.com/ishida/photos" aria-label="写真"><svg aria-hidden="true"><use href="#team-icon-camera" /></svg></a></li>
          <li><a href="https://example.com/ishida" aria-label="ウェブサイト"><svg aria-hidden="true"><use href="#team-icon-globe" /></svg></a></li>
          <li><a href="mailto:sample@higashiyuki.com" aria-label="メール"><svg aria-hidden="true"><use href="#team-icon-mail" /></svg></a></li>
        </ul>
      </div>
      <h3 class="member__name">石田 恵理</h3>
      <p class="member__role">フロントエンドエンジニア</p>
    </li>
  </ul>
</section>

CSS
.team {
  --ground: #111111;      /* 地 */
  --ink: #eeece6;         /* 名前と見出し */
  --mute: #a8a6a0;        /* 肩書きとラベル */
  --chip: #ffffff;        /* アイコンの丸 */
  --chip-ink: #111111;    /* アイコンの線 */
  /* 写真の休みとホバーの filter。数と並びをそろえると、GSAP が数ごとに補間する */
  --rest: grayscale(1) brightness(0.72);
  --hover: grayscale(0) brightness(1);
  padding: 64px clamp(16px, 6vw, 96px);
  background: var(--ground);
  color: var(--ink);
}

.team__label { margin: 0; color: var(--mute); font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; }
.team__title { margin: 16px 0 48px; font-size: clamp(32px, 5vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }

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

/* アイコンはこの枠の下端の外に隠す */
.member__photo {
  position: relative;
  aspect-ratio: 31 / 44;
  border-radius: 4px;
  overflow: hidden;
}

.member__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--tint);
  filter: var(--rest);
}

.member__social {
  position: absolute;
  left: 16px;
  bottom: 16px;
  display: flex;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.member__social a {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--chip);
  color: var(--chip-ink);
}

.member__social svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.member__social a:focus-visible {
  outline: 2px solid var(--chip);
  outline-offset: 3px;
}

.member__name { margin: 20px 0 4px; font-size: 24px; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em; }
.member__role { margin: 0; color: var(--mute); font-size: 14px; line-height: 1.7; letter-spacing: 0.04em; }

/* ホバーの無い端末(指で触る)では、はじめから色とアイコンを見せる */
@media (hover: none) {
  .member__photo img { filter: none; }
}

JavaScript
import { gsap } from "gsap";

const LEAVE = 2; // 離れたときに戻る速さ(入るときの倍率)。隣へ移ったとき、前の人の色が残らない
const canHover = window.matchMedia("(hover: hover)").matches;
// 動きを減らす設定では、ほぼ一瞬で切り替える
const speed = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 20 : 1;

// 指で触る端末では、色とアイコンをはじめから見せたままにする(CSS の hover: none)
if (canHover) {
  document.querySelectorAll(".member").forEach((member) => {
    const style = getComputedStyle(member);
    const photo = member.querySelector(".member__photo img");
    const icons = member.querySelectorAll(".member__social a");
    // 写真の下端の外まで下げておく(アイコンの大きさ+下の余白)
    const rise = icons[0].offsetHeight + parseFloat(getComputedStyle(icons[0].closest("ul")).bottom);

    const hover = gsap
      .timeline({ paused: true })
      // 行きと帰りで同じ形になる inOut の ease(out を逆にたどると、離れてもしばらく色が残る)
      .fromTo(photo, { filter: style.getPropertyValue("--rest").trim() }, { filter: style.getPropertyValue("--hover").trim(), duration: 0.6, ease: "sine.inOut" }, 0)
      .fromTo(icons, { y: rise }, { y: 0, duration: 0.45, ease: "back.out(1.6)", stagger: 0.06 }, 0.08);

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

    member.addEventListener("pointerenter", show);
    member.addEventListener("pointerleave", hide);
    // キーボードでアイコンへ移ったときも、ホバーと同じように出す
    member.addEventListener("focusin", show);
    member.addEventListener("focusout", hide);
  });
}

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

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