Insights

技術情報

CSS+JSで作る無限マーキー(横・縦対応)

ポイントは「1セット分の幅(または高さ)」をJSで正確に計測し、CSS変数 --marquee-step に流し込むこと。
CSS側はその距離分だけ transform するだけで、計算不要の無限スクロールが成立します。

HTML

<div class="marquee">
  <div class="marquee__inner">
    <div class="marquee__track js-marquee">
      <span class="marquee__item">コンテンツA</span>
      <span class="marquee__item">コンテンツB</span>
      <span class="marquee__item">コンテンツC</span>
    </div>
  </div>
</div>

SCSS

GPUアクセラレーションを有効にするため、will-change プロパティを追加しています。

.marquee {
  overflow: hidden;
  // 必要に応じて幅や高さを指定
  width: 100%;
}

.marquee__inner {
  overflow: hidden;
  width: 100%;
  height: 100%;
}

.marquee__track {
  display: flex;
  align-items: center;
  width: fit-content;
  gap: 12px;

  // 初期状態ではアニメーションさせない(JSで計算後に開始)
  animation: none;
  will-change: transform; 

  --marquee-duration: 20s;
  --marquee-clone-length: 2;
  --marquee-step: 0px; // JSで上書き

  &.is-vertical {
    flex-direction: column;
    width: 100%; // 縦の場合は幅を埋める
    height: fit-content;
  }
}

.marquee__item {
  flex-shrink: 0;
  white-space: nowrap;
}

/* 横スクロール */
@keyframes marquee-horizontal {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(calc(-1 * var(--marquee-step)));
  }
}

/* 縦スクロール */
@keyframes marquee-vertical {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(calc(-1 * var(--marquee-step)));
  }
}

JS

/**
 * 無限マーキー初期化
 * - track内の要素を複製して画面を埋める
 * - 1セット分の幅/高さを --marquee-step に設定
 * - CSS keyframes でアニメーション実行
 */
const initMarquee = (selector = ".js-marquee") => {
  const tracks = document.querySelectorAll(selector);
  if (!tracks.length) return;

  const setupTrack = (track) => {
    // リサイズ時の再計算用に初期HTMLを保持
    const baseHtml = track.dataset.marqueeBaseHtml || track.innerHTML;
    if (!track.dataset.marqueeBaseHtml) {
      track.dataset.marqueeBaseHtml = baseHtml;
    }
    track.innerHTML = baseHtml;

    const items = Array.from(track.children);
    if (!items.length) return;

    const container = track.parentElement;
    if (!container) return;

    const isVertical = track.classList.contains("is-vertical");
    const axis = isVertical ? "y" : "x";

    // コンテナとコンテンツのサイズ計測
    // 縦向きの場合は高さを、横向きの場合は幅を基準にする
    const containerSize = axis === "x" ? container.clientWidth : container.clientHeight;
    let contentSize = axis === "x" ? track.scrollWidth : track.scrollHeight;

    // コンテンツがコンテナの2倍以上になるまで複製(最低1回は複製)
    let cloneCount = 0;
    const maxClones = 12; // 無限ループ防止

    while ((contentSize < containerSize * 2 || cloneCount < 1) && cloneCount < maxClones) {
      items.forEach((item) => {
        const clone = item.cloneNode(true);
        clone.setAttribute("aria-hidden", "true"); // アクセシビリティ対応
        track.appendChild(clone);
      });
      cloneCount++;
      // 複製後のサイズを再取得
      contentSize = axis === "x" ? track.scrollWidth : track.scrollHeight;
    }

    // 1セット分の移動距離を算出
    const cloneLength = cloneCount + 1;
    track.style.setProperty("--marquee-clone-length", String(cloneLength));

    if (contentSize > 0) {
      const stepSize = contentSize / cloneLength;
      track.style.setProperty("--marquee-step", `${stepSize}px`);
    }

    // アニメーション適用
    const animationName = isVertical ? "marquee-vertical" : "marquee-horizontal";
    track.style.animation = `${animationName} var(--marquee-duration) linear infinite`;
    track.dataset.marqueeInitialized = "true";
  };

  const setupAll = () => tracks.forEach(setupTrack);

  // フォント読み込み完了後に計算(ズレ防止)
  if (document.fonts && document.fonts.ready) {
    document.fonts.ready.then(setupAll);
  } else {
    setupAll();
  }

  // リサイズ対応
  let resizeTimer;
  window.addEventListener("resize", () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(setupAll, 200);
  });
};

window.addEventListener("DOMContentLoaded", () => {
  initMarquee();
});

運用・実装のポイント

  1. 縦スクロール対応
    HTMLの js-marquee 要素に is-vertical クラスを付与するだけで縦方向に切り替わります。
  2. 速度調整
    CSS変数の --marquee-duration(例: 20s)を変更することで、JSを触らずに速度調整が可能です。
  3. アクセシビリティ
    複製された要素には自動的に aria-hidden="true" が付与されるため、スクリーンリーダーによる読み上げの重複を防ぎます。
  4. パフォーマンス
    transform プロパティによる移動と will-change の指定により、描画負荷を抑えたスムーズな動作を実現しています。