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

ヒーローの巨大な見出しが傾いた帯になって横に流れ続ける

デモ

仕様

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

しくみと調整

「文言 + 区切りの星」を1つの単位として並べ、コピーを複製してから xPercent で自分の幅ぶんだけ動かして巻き戻します。1組ぶん動いた見た目は動く前の見た目と重なるので、巻き戻しても継ぎ目が見えず、途切れずに流れ続けて見えます。

3本の帯は向きと見た目を変えています。塗りの帯・地の色を反転した帯・輪郭だけの帯を並べ、輪郭の帯は paint-order で線を先に描いてから塗ることで、可変フォントでも内側の継ぎ目が出ないようにしています。

複製した単位は読み上げから外し、最初の1つだけを残しています。5秒より長く動き続けるので、ホバーでゆっくり止まるだけでなく、キーボードや指でも止められる一時停止のボタンを置いています。動きを減らす設定では複製をせず、最初の1組を並べたまま止めています。

帯の数や向き、字の大きさを変えるだけで、勢いの強さを調整できます。イベント・ファッション・エージェンシーのように、強い印象を残したいヒーローの見出しに向いています。

コード

HTML
<section class="marquee-hero">
  <h1 class="marquee-hero__heading">
    <span class="marquee-hero__row" data-style="solid">
      <span class="marquee-hero__unit">動きで、伝える<i class="marquee-hero__dot" aria-hidden="true"></i></span>
    </span>
    <span class="marquee-hero__row" data-style="invert" data-direction="right">
      <span class="marquee-hero__unit">記憶に残るデザイン<i class="marquee-hero__dot" aria-hidden="true"></i></span>
    </span>
    <span class="marquee-hero__row" data-style="outline">
      <span class="marquee-hero__unit">かたちにして、届ける<i class="marquee-hero__dot" aria-hidden="true"></i></span>
    </span>
  </h1>

  <!-- 5 秒より長く動き続けるものには、止める手段が要る。JS が出す -->
  <button class="marquee-hero__toggle" type="button" aria-pressed="false" hidden>一時停止</button>
</section>

CSS
.marquee-hero {
  --bg: #f2f1ed;
  --ink: #262626;
  --size: clamp(40px, 9vw, 112px);
  overflow: hidden;
  padding: 96px 0;
  background: var(--bg);
  color: var(--ink);
  text-align: center;
}

.marquee-hero__heading {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 -6vw;
  transform: rotate(-3deg); /* 帯全体をわずかに傾ける */
  font-size: var(--size);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
}

.marquee-hero__row {
  display: flex;
  overflow: hidden;
  white-space: nowrap;
}

.marquee-hero__row[data-style="invert"] { background: var(--ink); color: var(--bg); }

.marquee-hero__row[data-style="outline"] .marquee-hero__unit {
  color: var(--bg);
  /* 可変フォントは字形の輪郭が重なるので、線を先に描いてから塗る(内側の継ぎ目が出ない) */
  -webkit-text-stroke: max(3px, 0.03em) var(--ink);
  paint-order: stroke fill;
}

.marquee-hero__unit {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 0.3em;
  padding-right: 0.3em;
}

.marquee-hero__dot {
  flex: none;
  width: 0.42em;
  height: 0.42em;
  background: currentColor;
  clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
}

.marquee-hero__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;
}

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

JavaScript
import { gsap } from "gsap";

const DURATION = 8; // 1 組ぶん動くのにかかる秒数
const EASE_TIME = 0.5; // 止まるまで・動き出すまでの時間(秒)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".marquee-hero").forEach((hero) => {
  // 動きを減らす設定では流さず、最初の 1 組を並べたまま止めておく
  if (reduceMotion) return;
  const toggle = hero.querySelector(".marquee-hero__toggle");
  let paused = false;

  // 帯の幅は書体で決まるので、読み込みを待ってから複製する
  document.fonts.ready.then(() => {
    const loops = [...hero.querySelectorAll(".marquee-hero__row")].map((row) => {
      const unit = row.querySelector(".marquee-hero__unit");
      // いちばん広い画面でも途切れないよう、足りるだけ複製する
      const copies = Math.ceil(Math.max(screen.width, innerWidth) / unit.offsetWidth) + 1;
      for (let i = 0; i < copies; i++) row.append(unit.cloneNode(true));
      // 複製は読み上げから外す(最初の 1 つだけ残す)
      [...row.querySelectorAll(".marquee-hero__unit")].slice(1).forEach((node) => node.setAttribute("aria-hidden", "true"));
      const units = row.querySelectorAll(".marquee-hero__unit");
      // どの組も自分の幅(xPercent の 100)だけ動かして巻き戻す。1 組ぶん動くと元の絵に重なるので継ぎ目が出ない
      const toRight = row.dataset.direction === "right";
      const loop = gsap.fromTo(
        units,
        { xPercent: toRight ? -100 : 0 },
        { xPercent: toRight ? 0 : -100, duration: DURATION, ease: "none", repeat: -1 }
      );
      // ホバーしている間はゆっくり止める。止めるボタンが押されていれば、離れても止めたまま
      const ease = (to) => gsap.to(loop, { timeScale: to, duration: EASE_TIME, ease: "power2.out", overwrite: true });
      row.addEventListener("pointerenter", () => ease(0));
      row.addEventListener("pointerleave", () => ease(paused ? 0 : 1));
      return ease;
    });

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

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

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