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

カードが途切れず横に流れ続けるカルーセル

デモ

仕様

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

しくみと調整

カードを敷き詰めた帯を横に流しながら、それぞれのカードの位置を modifiers で折り返します。左端から出たカードは右端の外に回り込むので、複製を作らなくても途切れずに流れ続けます。

流す速さは 1 秒に 60px で、カードの並びと間隔から 1 周にかかる時間を決めています。止まる・動き出すまでの時間は 0.6 秒で、これより短くすると急に止まって見えます。

折り返す範囲は「表示幅 + カード 1 枚ぶん」以上が必要です。足りないと、回り込んだカードが画面の中に現れてしまいます。カードの幅と間隔は、CSS で決めた値を実測して使い、JS には書き込みません。

ホバーすると流れる速さがゆっくり落ちて読みやすくなり、離れると元の速さに戻ります。キーボードや指で操作する人のために、止めるボタンも用意しています。

記事・事例・お客様の声のような、横に流し続けたいカード列に向いています。カードの枚数や幅がそろっていなくても使えます。

コード

HTML
<section class="carousel" aria-labelledby="carousel-title">
  <h2 class="carousel__title" id="carousel-title">読みもの</h2>

  <ul class="carousel__track">
    <li class="carousel__card">
      <img class="carousel__image" src="/images/journal-travel.jpg" alt="" width="336" height="240">
      <p class="carousel__tag">旅</p>
      <h3 class="carousel__card-title">静かな町で過ごす、12時間</h3>
      <p class="carousel__time">6分で読めます</p>
    </li>
    <li class="carousel__card">
      <img class="carousel__image" src="/images/journal-design.jpg" alt="" width="336" height="240">
      <p class="carousel__tag">デザイン</p>
      <h3 class="carousel__card-title">いまも紙にスケッチする理由</h3>
      <p class="carousel__time">4分で読めます</p>
    </li>
    <li class="carousel__card">
      <img class="carousel__image" src="/images/journal-food.jpg" alt="" width="336" height="240">
      <p class="carousel__tag">食</p>
      <h3 class="carousel__card-title">朝ごはんは、ゆっくりと</h3>
      <p class="carousel__time">5分で読めます</p>
    </li>
    <li class="carousel__card">
      <img class="carousel__image" src="/images/journal-work.jpg" alt="" width="336" height="240">
      <p class="carousel__tag">働き方</p>
      <h3 class="carousel__card-title">週4日勤務で、わかったこと</h3>
      <p class="carousel__time">8分で読めます</p>
    </li>
    <li class="carousel__card">
      <img class="carousel__image" src="/images/journal-craft.jpg" alt="" width="336" height="240">
      <p class="carousel__tag">ものづくり</p>
      <h3 class="carousel__card-title">紙とのりと、根気の話</h3>
      <p class="carousel__time">3分で読めます</p>
    </li>
  </ul>

  <button class="carousel__toggle" type="button" aria-pressed="false" hidden>一時停止</button>
</section>

CSS
.carousel {
  --bg: #f2f1ed;
  --surface: #ffffff;
  --ink: #262626;
  --mute: #6b6b6b;
  --line: rgba(38, 38, 38, 0.12);
  padding: 64px clamp(16px, 6vw, 96px) 96px;
  background: var(--bg);
  color: var(--ink);
  overflow: hidden;
}

.carousel__title {
  margin: 0 0 40px;
  font-size: clamp(28px, 4vw, 44px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.carousel__track {
  display: flex;
  flex-wrap: wrap;
  gap: 32px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.carousel__card {
  flex: none;
  width: 336px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line);
}

.carousel__image {
  display: block;
  width: 100%;
  height: 240px;
  object-fit: cover;
  background: var(--line);
}

.carousel__tag {
  width: fit-content;
  margin: 24px 24px 0;
  padding: 0 10px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--mute);
  font-size: 12px;
  font-weight: 700;
  line-height: 24px;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.carousel__card-title {
  margin: 16px 24px 0;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  word-break: auto-phrase;
}

.carousel__time {
  margin: 16px 24px 24px;
  color: var(--mute);
  font-size: 13px;
  letter-spacing: 0.04em;
}

.carousel__toggle {
  height: 40px;
  margin-top: 32px;
  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;
}

.carousel__toggle[aria-pressed="true"] { background: var(--ink); color: var(--bg); }

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

JavaScript
import { gsap } from "gsap";

const SPEED = 60; // 1 秒に流れる距離(px)
const EASE_TIME = 0.6; // 止まる・動き出すまでの時間(秒)
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".carousel").forEach((section) => {
  const track = section.querySelector(".carousel__track");
  const toggle = section.querySelector(".carousel__toggle");
  const cards = [...track.children];

  // 動きを減らす設定では流さず、折り返して全部を並べたままにする
  if (reduceMotion) return;

  // 間隔は CSS の gap から読む。寸法を JS に直書きしない
  const gap = parseFloat(getComputedStyle(track).gap) || 0;
  const pitch = cards[0].getBoundingClientRect().width + gap;
  const total = pitch * cards.length; // 1 組の幅(表示幅 + カード 1 枚ぶん以上にする)
  const wrapX = gsap.utils.wrap(-pitch, total - pitch);

  track.style.position = "relative";
  track.style.height = `${cards[0].offsetHeight}px`;
  cards.forEach((card, i) => {
    card.style.position = "absolute";
    card.style.left = "0";
    card.style.top = "0";
    gsap.set(card, { x: i * pitch });
  });

  // 各カードの x を「1 組の幅」で折り返す。左端から出たカードは右端の外へ回り込む
  const loop = gsap.to(cards, {
    x: `-=${total}`,
    duration: total / SPEED,
    ease: "none",
    repeat: -1,
    modifiers: { x: (x) => `${wrapX(parseFloat(x))}px` },
  });

  const ease = (to) => gsap.to(loop, { timeScale: to, duration: EASE_TIME, ease: "power2.out", overwrite: true });
  let paused = false;
  track.addEventListener("pointerenter", () => ease(0));
  track.addEventListener("pointerleave", () => ease(paused ? 0 : 1));

  // キーボードでも押せる止めるボタン
  toggle.hidden = false;
  toggle.addEventListener("click", () => {
    paused = !paused;
    toggle.setAttribute("aria-pressed", String(paused));
    ease(paused ? 0 : 1);
  });
});

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

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