本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
セクション

セクションの取引先ロゴの壁が、1 枚ずつ縦にめくれて入れ替わり続ける

デモ

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

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

取引先のロゴを白いタイルに入れて格子に並べ、1 秒ごとにタイルを 1 枚選んで縦にめくります。タイルは奥へ倒れ、真横になった所で中のロゴを控えのロゴと入れ替えて、裏側から起き上がってきます。外したロゴは控えに戻すので、同じロゴが 2 つ並ぶことはありません。並べきれない数の取引先を、同じ場所で順に見せられます。

めくる時間は 0.9 秒です。倒れる前半は動き出しをゆっくり、起き上がる後半は着く所をゆっくりにして(sine.in と sine.out)、真横を過ぎるときがいちばん速く見えるようにしています。遠近の距離(transformPerspective)を 520px にすると、倒れる縁が手前へ少しせり出して、板が回っているように見えます。倒れるほど濃くなる陰を重ね(真横で 45%)、白いタイルでも向きが変わったことが分かるようにしています。

控えのロゴは template の中に置いています。中の画像は表示も読み込みもされず、読み上げにも出ませんが、めくった瞬間に初めて読み込むと、起き上がったタイルが空のまま見えてしまいます。はじめに画像だけを先読みしておきます。めくるタイルは、最近めくった 3 枚を外して乱数で選びます。外さないと、同じ所が続けてめくれて、壁の一部だけが動いているように見えます。

5 秒より長く動き続けるものには、止める手段が要ります。壁にポインタを重ねると、めくる間隔を刻むトゥイーンの timeScale を 0.4 秒で 0 へ落として止め、離すと動き出します。キーボードや指で操作する人のために、一時停止のボタンも置いています。動きを減らす設定では、めくらずに最初の並びのままにします。

つまみは、めくる間隔と時間、陰の濃さ、続けて選ばない枚数です。間隔を 0.5 秒まで詰めると壁全体がざわついて見え、3 秒より空けると止まっているように見えます。ロゴを帯にして流すやり方と違い、格子の位置が変わらないので、取引先の多い導入実績の節や、メディア掲載・受賞歴の一覧に向いています。

コード

HTML
<section class="logo-wall" aria-labelledby="logo-wall-title">
  <p class="logo-wall__eyebrow">Customers</p>
  <h2 class="logo-wall__title" id="logo-wall-title">多くのチームに<wbr>選ばれています</h2>
  <p class="logo-wall__lead">ここに説明文が入ります。</p>

  <!-- 並んでいるロゴ。めくれるのは .logo-wall__card で、中の img が入れ替わる -->
  <ul class="logo-wall__grid">
    <li><div class="logo-wall__card"><img src="/logos/aldermoor.svg" alt="Aldermoor" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/brightwell.svg" alt="Brightwell" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/corvine.svg" alt="Corvine" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/dunmere.svg" alt="dunmere" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/everloop.svg" alt="Everloop" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/fernhill.svg" alt="Fernhill & Co" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/glasswing.svg" alt="Glasswing" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/halden.svg" alt="Halden" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/ironbark.svg" alt="Ironbark" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/juniper.svg" alt="juniper" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/kestrow.svg" alt="Kestrow" width="160" height="40"></div></li>
    <li><div class="logo-wall__card"><img src="/logos/lumenrow.svg" alt="Lumenrow" width="160" height="40"></div></li>
  </ul>

  <!-- 控えのロゴ。template の中は表示も読み込みもされない。めくるたびに、タイルのロゴと 1 つずつ入れ替わる -->
  <template class="logo-wall__pool">
    <img src="/logos/marlowe.svg" alt="Marlowe" width="160" height="40">
    <img src="/logos/northvale.svg" alt="Northvale" width="160" height="40">
    <img src="/logos/oakhaven.svg" alt="oakhaven" width="160" height="40">
    <img src="/logos/pellucid.svg" alt="Pellucid" width="160" height="40">
    <img src="/logos/quillon.svg" alt="Quillon" width="160" height="40">
    <img src="/logos/rookery.svg" alt="rookery" width="160" height="40">
    <img src="/logos/saltmarsh.svg" alt="Saltmarsh" width="160" height="40">
    <img src="/logos/tallis.svg" alt="Tallis" width="160" height="40">
  </template>

  <div class="logo-wall__foot">
    <a class="logo-wall__link" href="/customers/">導入事例を見る →</a>
    <!-- 5 秒より長く動き続けるので、止める手段を置く。めくるときだけ JS が出す -->
    <button class="logo-wall__toggle" type="button" aria-pressed="false" hidden>一時停止</button>
  </div>
</section>

CSS
.logo-wall {
  --bg: #f2f1ed;
  --surface: #ffffff;
  --ink: #262626;
  --mute: #6b6b6b;
  --card-height: 104px;
  padding: 96px 24px;
  background: var(--bg);
  color: var(--ink);
  text-align: center;
}

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

.logo-wall__title {
  margin: 0 0 16px;
  font-size: clamp(28px, 4vw, 44px);
  font-weight: 800;
  line-height: 1.1; letter-spacing: -0.02em;
  word-break: keep-all; /* 折り返すのは HTML の <wbr> の所だけ */
}

.logo-wall__lead { margin: 0 0 40px; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; }

.logo-wall__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  max-width: 1088px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.logo-wall__card {
  position: relative;
  display: grid;
  place-items: center;
  height: var(--card-height);
  border-radius: 4px;
  background: var(--surface);
}

/* めくれるタイルの陰。--shade を JS が 0〜0.45 で動かす */
.logo-wall__card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: rgb(38 38 38 / var(--shade, 0));
}

.logo-wall__card img { display: block; width: auto; max-width: 80%; height: 32px; }

.logo-wall__foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: 1088px;
  margin: 32px auto 0;
}

.logo-wall__link {
  color: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-underline-offset: 0.4em;
}

.logo-wall__toggle {
  height: 40px;
  padding: 0 20px;
  border: 0;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--ink);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
}

.logo-wall__toggle[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
.logo-wall :focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

@media (max-width: 720px) {
  .logo-wall { --card-height: 80px; }
  .logo-wall__grid { grid-template-columns: repeat(2, 1fr); }
}

JavaScript
import { gsap } from "gsap";

const STEP = 1; // めくりの間隔(秒)
const FLIP = 0.9; // 1 枚がめくれる時間(秒)
const SHADE = 0.45; // 真横まで倒れたときの陰の濃さ
const REST = 3; // めくったタイルを、次に選ばないでおく枚数
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".logo-wall").forEach((wall) => {
  // 動きを減らす設定では、めくらずに最初の並びのままにする
  if (reduceMotion) return;
  const grid = wall.querySelector(".logo-wall__grid");
  const cards = [...wall.querySelectorAll(".logo-wall__card")];
  const pool = [...wall.querySelector(".logo-wall__pool").content.children];
  const toggle = wall.querySelector(".logo-wall__toggle");
  const recent = [];
  let hovering = false;
  let paused = false;

  // 控えの画像は template の中では読み込まれない。めくった瞬間に空にならないよう、先に読んでおく
  pool.forEach((img) => (new Image().src = img.getAttribute("src")));
  gsap.set(cards, { transformPerspective: 520 });

  function flipOne() {
    const choices = cards.filter((card) => !recent.includes(card));
    const card = choices[Math.floor(Math.random() * choices.length)];
    recent.push(card);
    if (recent.length > REST) recent.shift();
    const next = pool.splice(Math.floor(Math.random() * pool.length), 1)[0];
    gsap
      .timeline()
      // 奥へ倒れ、真横になった所で中身を入れ替えて、裏から起き上がる
      .to(card, { rotationX: 90, "--shade": SHADE, duration: FLIP / 2, ease: "sine.in" })
      .add(() => {
        pool.push(card.firstElementChild);
        card.replaceChildren(next);
      })
      .fromTo(card, { rotationX: -90 }, { rotationX: 0, "--shade": 0, duration: FLIP / 2, ease: "sine.out" });
  }

  // 間隔を刻むだけのトゥイーン。timeScale を 0 にすると次のめくりが来なくなる(めくりかけは最後までめくる)
  const loop = gsap.to({}, { duration: STEP, repeat: -1, onRepeat: flipOne });
  const update = () => gsap.to(loop, { timeScale: hovering || paused ? 0 : 1, duration: 0.4, overwrite: true });

  grid.addEventListener("pointerenter", () => {
    hovering = true;
    update();
  });
  grid.addEventListener("pointerleave", () => {
    hovering = false;
    update();
  });

  // キーボードや指でも止められるボタン。名前は変えず、押されているかを aria-pressed で伝える
  toggle.hidden = false;
  toggle.addEventListener("click", () => {
    paused = !paused;
    toggle.setAttribute("aria-pressed", String(paused));
    update();
  });
});

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

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