デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ロゴ 1 組を並べたリストを、画面の幅を埋めるだけ複製して横につなぎ、どの組も自分の幅の分だけ(xPercent で -100)左へ動かしては巻き戻す動きを繰り返します。1 組ぶん動いたときの見た目は、動く前の見た目とまったく同じになるので、巻き戻しても継ぎ目が見えず、途切れずに流れ続けて見えます。2 段目は向きを逆にして、帯に奥行きを出しています。
速さは 1 秒に 60px です。1 周の時間を組の幅から決めるので、段ごとにロゴの数や幅が違っても、同じ速さでそろって流れます。ロゴの左右には同じ幅の余白を付け、組と組の継ぎ目も、ロゴとロゴの間と同じ幅にします。帯の両端は、幅の 14% ずつを mask-image のグラデーションで薄くして、ロゴが端から湧いて端へ消えていくように見せています。
複製した組は、読み上げから外します(aria-hidden)。外さないと、同じ取引先の名前が何度も読み上げられます。複製の数は、組の幅を測ってから決めます。ロゴの画像に幅と高さの属性を書いておかないと、読み込む前の幅で測ってしまい、複製が足りずに帯の右側が空きます。
5 秒より長く動き続けるものには、止める手段が要ります。ホバーでは timeScale を 0 へ補間して、その段をゆっくり止めます。ただ、ホバーではキーボードや指で操作する人が止められないので、一時停止のボタンも置いておきます。動きを減らす設定では流さずに、ロゴを折り返して全部並べたままにします。
つまみは、速さ、ロゴの高さと余白、両端を薄くする幅です。速すぎるとロゴが読めず、遅すぎると止まっているように見えるので、流れながらでも名前が読み取れる速さに合わせます。ヒーローのすぐ下に置く導入実績や取引先、メディア掲載の一覧に向いています。
コード
<section class="logo-marquee" aria-labelledby="logo-marquee-title">
<p class="logo-marquee__eyebrow">CLIENTS</p>
<h2 class="logo-marquee__title" id="logo-marquee-title">4,000社以上に<span>選ばれています</span></h2>
<!-- 1 段 = ロゴ 1 組のリスト。流すための複製は JS が作り、読み上げからは外す。
スクリプトが無いときや動きを減らす設定では、折り返して全部を並べたまま -->
<div class="logo-marquee__row">
<ul class="logo-marquee__list">
<li><img src="/logos/orbit.svg" alt="orbit" width="136" height="40"></li>
<li><img src="/logos/prism.svg" alt="Prism" width="136" height="40"></li>
<li><img src="/logos/summit.svg" alt="Summit" width="148" height="40"></li>
<li><img src="/logos/beacon.svg" alt="beacon" width="140" height="40"></li>
<li><img src="/logos/cascade.svg" alt="Cascade" width="156" height="40"></li>
<li><img src="/logos/vertex.svg" alt="Vertex" width="152" height="40"></li>
<li><img src="/logos/nimbus.svg" alt="nimbus" width="144" height="40"></li>
<li><img src="/logos/harbor.svg" alt="Harbor & Co" width="184" height="40"></li>
</ul>
</div>
<!-- data-direction="right" の段は、右へ流れる -->
<div class="logo-marquee__row" data-direction="right">
<ul class="logo-marquee__list">
<li><img src="/logos/kestrel.svg" alt="Kestrel" width="148" height="40"></li>
<li><img src="/logos/quill.svg" alt="quill" width="120" height="40"></li>
<li><img src="/logos/aurora.svg" alt="Aurora" width="148" height="40"></li>
<li><img src="/logos/fable.svg" alt="Fable" width="128" height="40"></li>
<li><img src="/logos/lattice.svg" alt="Lattice" width="148" height="40"></li>
<li><img src="/logos/solace.svg" alt="solace" width="136" height="40"></li>
<li><img src="/logos/ember.svg" alt="Ember & Oak" width="184" height="40"></li>
<li><img src="/logos/tidal.svg" alt="Tidal" width="120" height="40"></li>
</ul>
</div>
<!-- 5 秒より長く動き続けるものには、止める手段を置く。流すときだけ JS が出す -->
<button class="logo-marquee__toggle" type="button" aria-pressed="false" hidden>一時停止</button>
</section>
.logo-marquee {
--bg: #111111;
--ink: #eeece6;
--mute: #a8a6a0;
--line: rgb(238 236 230 / 0.14);
--logo-height: 40px;
--space: 48px; /* ロゴの左右の余白。ロゴとロゴの間は、この 2 倍になる */
--fade: 14%; /* 帯の両端を薄くする幅 */
padding: 96px 0;
background: var(--bg);
color: var(--ink);
text-align: center;
}
.logo-marquee__eyebrow {
margin: 0 0 24px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--mute);
}
.logo-marquee__title {
margin: 0 0 56px;
padding: 0 24px;
font-size: clamp(28px, 4vw, 44px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
}
/* 狭い画面では、語の途中ではなく「〜に/選ばれています」の切れ目で折り返す */
.logo-marquee__title span { display: inline-block; }
.logo-marquee__row {
display: flex;
justify-content: center;
padding: 24px 0;
border-block: 1px solid var(--line);
}
.logo-marquee__row + .logo-marquee__row { margin-top: 32px; }
.logo-marquee__list {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
row-gap: 24px;
margin: 0;
padding: 0;
list-style: none;
}
/* 余白はロゴの左右に同じだけ付ける。組と組の継ぎ目も、ロゴの間と同じ幅になる */
.logo-marquee__list li { flex: none; padding: 0 var(--space); }
.logo-marquee__list img { display: block; width: auto; height: var(--logo-height); }
/* 流すときは 1 行に並べ、はみ出た分を隠して両端を薄くする */
.logo-marquee.is-running .logo-marquee__row {
justify-content: flex-start;
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
mask-image: linear-gradient(90deg, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
}
.logo-marquee.is-running .logo-marquee__list { flex: none; flex-wrap: nowrap; }
.logo-marquee__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;
}
.logo-marquee__toggle[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
.logo-marquee__toggle:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
import { gsap } from "gsap";
const SPEED = 60; // 1 秒に流れる距離(px)。段ごとに組の幅が違っても、同じ速さで流れる
const EASE_TIME = 0.6; // 止まるまで・動き出すまでの時間(秒)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".logo-marquee").forEach((marquee) => {
// 動きを減らす設定では流さず、折り返して全部を並べたままにする
if (reduceMotion) return;
marquee.classList.add("is-running");
const toggle = marquee.querySelector(".logo-marquee__toggle");
let paused = false;
const loops = [...marquee.querySelectorAll(".logo-marquee__row")].map((row) => {
const list = row.querySelector(".logo-marquee__list");
// いちばん広い画面でも途切れないよう、足りるだけ組を複製する。複製は読み上げから外す
const copies = Math.ceil(Math.max(screen.width, innerWidth) / list.offsetWidth) + 1;
for (let i = 0; i < copies; i++) {
const clone = list.cloneNode(true);
clone.setAttribute("aria-hidden", "true");
row.append(clone);
}
// どの組も自分の幅(xPercent の 100)だけ動かして巻き戻す。1 組ぶん動くと元の絵に重なるので、継ぎ目が出ない
const lists = row.querySelectorAll(".logo-marquee__list");
const toRight = row.dataset.direction === "right";
const loop = gsap.fromTo(
lists,
{ xPercent: toRight ? -100 : 0 },
{ xPercent: toRight ? 0 : -100, duration: list.offsetWidth / SPEED, ease: "none", repeat: -1 }
);
// ホバーしている間は、ゆっくり止める(timeScale を 0 へ)。止めるボタンが押されていれば、離れても止めたまま
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));
});
});