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

ホバーでカードが 3D に裏返って裏面が出る

デモ

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

仕様

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

しくみと調整

表と裏の 2 面を同じ位置に重ね、両方に backface-visibility の hidden を付けて、裏面だけをはじめから 180° 回しておきます。2 面を包む要素を Y 軸で 0° から 180° まで回すと、表が向こうへ隠れ、裏が手前に出てきます。

回転は 0.8 秒の power3.inOut で、止めたタイムライン 1 本にまとめ、カーソルが重なったら play()、離れたら reverse() します。途中で離れても、その角度から引き返します。奥行き(perspective)は回らない外枠に 1200px で付け、値を小さくすると遠近が強くなります。

カーソルは、回る要素ではなく、回らない外枠で拾います。回る要素で拾うと、真横を向いた瞬間に幅がなくなって離れたことになり、表と裏をちらちらと行き来します。

裏面は、表を向いているあいだも visibility や aria-hidden で隠しません。隠すと、裏面の文やリンクが読み上げと Tab の移動から外れます。フォーカスがカードの中に入ったら裏返し、外へ出たら表に戻すと、キーボードでも裏面を読めます。指で触る端末にはホバーが無いので、タップで裏返します。

縦に回すなら、回す軸を X にして、裏面も X 軸で 180° 回しておきます。チーム紹介、料金プラン、単語カード、商品の表と詳細に向いています。

コード

HTML
<section class="team" aria-labelledby="team-title">
  <h2 class="team__title" id="team-title">メンバー紹介</h2>
  <p class="team__lead">カードにカーソルを合わせてみてください。</p>

  <ul class="team__list">
    <!-- 回らない外枠。ホバーとフォーカスを拾い、奥行き(perspective)を持つ。--tint は写真が届くまでの地の色 -->
    <li class="flip-card" style="--tint: #b27771">
      <div class="flip-card__inner">
        <div class="flip-card__face flip-card__front">
          <img class="flip-card__photo" src="/img/team-takahashi.jpg" alt="" width="640" height="600">
          <h3 class="flip-card__name">高橋 美緒</h3>
          <p class="flip-card__role">デザインリード</p>
        </div>
        <!-- 裏面は、表を向いているあいだも読み上げとキーボードの対象のまま(visibility や aria-hidden で隠さない) -->
        <div class="flip-card__face flip-card__back">
          <p class="flip-card__hello">美緒からひとこと</p>
          <p class="flip-card__bio">ここに説明文が入ります。</p>
          <ul class="flip-card__skills" aria-label="得意なこと">
            <li>UI設計</li>
            <li>デザインシステム</li>
          </ul>
          <a class="flip-card__link" href="/contact/?to=takahashi">メッセージを送る<span class="flip-card__sr">(高橋 美緒)</span> <span aria-hidden="true">→</span></a>
        </div>
      </div>
    </li>

    <li class="flip-card" style="--tint: #688297">
      <div class="flip-card__inner">
        <div class="flip-card__face flip-card__front">
          <img class="flip-card__photo" src="/img/team-morita.jpg" alt="" width="640" height="600">
          <h3 class="flip-card__name">森田 蓮</h3>
          <p class="flip-card__role">モーションデザイナー</p>
        </div>
        <div class="flip-card__face flip-card__back">
          <p class="flip-card__hello">蓮からひとこと</p>
          <p class="flip-card__bio">ここに説明文が入ります。</p>
          <ul class="flip-card__skills" aria-label="得意なこと">
            <li>モーション</li>
            <li>3D</li>
          </ul>
          <a class="flip-card__link" href="/contact/?to=morita">メッセージを送る<span class="flip-card__sr">(森田 蓮)</span> <span aria-hidden="true">→</span></a>
        </div>
      </div>
    </li>

    <li class="flip-card" style="--tint: #739374">
      <div class="flip-card__inner">
        <div class="flip-card__face flip-card__front">
          <img class="flip-card__photo" src="/img/team-ono.jpg" alt="" width="640" height="600">
          <h3 class="flip-card__name">小野 彩花</h3>
          <p class="flip-card__role">フロントエンドエンジニア</p>
        </div>
        <div class="flip-card__face flip-card__back">
          <p class="flip-card__hello">彩花からひとこと</p>
          <p class="flip-card__bio">ここに説明文が入ります。</p>
          <ul class="flip-card__skills" aria-label="得意なこと">
            <li>Web制作</li>
            <li>表示速度の改善</li>
          </ul>
          <a class="flip-card__link" href="/contact/?to=ono">メッセージを送る<span class="flip-card__sr">(小野 彩花)</span> <span aria-hidden="true">→</span></a>
        </div>
      </div>
    </li>
  </ul>
</section>

CSS
.team {
  --ground: #f2f1ed;       /* 地 */
  --surface: #ffffff;      /* 表の面 */
  --ink: #262626;          /* 表の文字。裏の面の色にもなる(明暗の反転) */
  --mute: #6b6b6b;         /* 役職 */
  --back-ink: #eeece6;     /* 裏の文字 */
  --back-mute: #a8a6a0;    /* 裏の小さいラベル */
  --back-line: rgba(238, 236, 230, 0.14); /* 裏のチップの枠 */
  padding: 48px clamp(24px, 6vw, 96px);
  background: var(--ground);
  color: var(--ink);
}

.team__title { margin: 0; font-size: clamp(32px, 5vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
.team__lead { margin: 8px 0 40px; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }

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

/* 回らない外枠。奥行きはここで付ける(値が小さいほど遠近が強い) */
.flip-card { perspective: 1200px; }

/* 表と裏を同じ升に重ねる。カードの高さは、中身の多いほうの面に合う */
.flip-card__inner {
  display: grid;
  transform-style: preserve-3d;
}

.flip-card__face {
  grid-area: 1 / 1;
  border-radius: 4px;
  overflow: hidden;
  backface-visibility: hidden;
}

.flip-card__front {
  padding-bottom: 24px;
  background: var(--surface);
  color: var(--ink);
}

.flip-card__photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 15;
  object-fit: cover;
  background: var(--tint);
}

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

/* 裏は、明暗を反転した面。はじめから裏向きに重ねておく */
.flip-card__back {
  display: flex;
  flex-direction: column;
  padding: 24px;
  background: var(--ink);
  color: var(--back-ink);
  transform: rotateY(180deg);
}

.flip-card__hello { margin: 0; color: var(--back-mute); font-size: 13px; font-weight: 700; letter-spacing: 0.06em; line-height: 1; }
/* 和文を文節の切れ目で折り返す(対応していないブラウザでは、ふつうに折り返す) */
.flip-card__bio { margin: 16px 0 auto; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }

.flip-card__skills { display: flex; flex-wrap: wrap; gap: 8px; margin: 24px 0; padding: 0; list-style: none; }

/* 得意なことのチップ(和文の小さなラベル) */
.flip-card__skills li {
  padding: 0 12px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--back-line);
  color: var(--back-mute);
  font-size: 12px;
  font-weight: 700;
  line-height: 24px;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.flip-card__link { align-self: start; color: inherit; font-size: 15px; font-weight: 600; letter-spacing: 0.04em; text-decoration: none; }
.flip-card__link:focus-visible { outline: 2px solid var(--back-ink); outline-offset: 4px; }

/* 読み上げにだけ届く語(同じ文言のリンクが並んでも、誰宛てか分かるように) */
.flip-card__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".flip-card").forEach((card) => {
  const inner = card.querySelector(".flip-card__inner");
  const flip = gsap
    .timeline({ paused: true })
    .fromTo(inner, { rotationY: 0 }, { rotationY: 180, duration: 0.8, ease: "power3.inOut" });

  // 動きを減らす設定では、ほぼ一瞬で裏返す
  if (reduceMotion) flip.timeScale(20);

  const open = () => flip.play();
  const close = () => flip.reverse();
  let touch = false;

  // ホバーは回らない外枠で拾う。回る面で拾うと、真横を向いた瞬間に幅がなくなり、離れたことになってちらつく
  card.addEventListener("pointerenter", (event) => event.pointerType !== "touch" && open());
  card.addEventListener("pointerleave", (event) => event.pointerType !== "touch" && close());

  // Tab で裏面のリンクへ移ったら裏返し、フォーカスがカードの外へ出たら表に戻す
  card.addEventListener("focusin", open);
  card.addEventListener("focusout", (event) => !card.contains(event.relatedTarget) && close());

  // 指で触る端末にはホバーが無いので、タップで裏返す(裏面のリンクを押したときは、そのまま移る)
  card.addEventListener("pointerdown", (event) => (touch = event.pointerType === "touch"));
  card.addEventListener("click", (event) => {
    if (!touch || event.target.closest("a")) return;
    flip.reversed() || flip.progress() === 0 ? open() : close();
  });
});

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

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