GODRICH

CSS 無限ループ マーキー9選 — CSSだけ・JS不要

css 無限ループは、同じ内容を2セット並べたトラックをちょうど半分だけ動かし、継ぎ目や巻き戻りを見せずに帯を流し続けるCSSの実装法です。この記事ではマーキー(横に流れる帯)を9パターン選び、JavaScriptを使わずtransformとanimationだけで組み立てました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

掲載順は効果の強さではなく、01の骨格をどう応用していくかで並べています。土台になる複製の公式(01)を実際の運用形(02)へ移し、そこに操作(03)と縁の処理(04)を足したあと、縦軸(05)と二方向(06)に広げ、速度のリズム(07)と奥行き(08)で仕上げて、最後にモーションを減らす設定でどう見えるか(09)へつなげました。実際のサービスでは1周12〜20秒が目安ですが、ここではグリッドの中で2秒周期に早送りしています。

01継ぎ目のないループの公式

A〜Fの6枚を同じ並びでもう一度複製してトラックに12枚並べ、transformを0から-50%までだけ動かすのが、このあと続く全パターンの土台になります。各チップにmargin-rightで同じ間隔を付けておくと、複製した1セット分の幅がトラック全体のちょうど半分になるため、戻った瞬間の位置が元の位置とぴったり重なります。

translateX(-50%)콘텐츠 2벌@keyframes
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 12s linear infinite;
}
.marquee__chip {
  margin-right: 14px;
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

02ロゴの無限ループ

NORTH・ACME・ORBITなど5つのワードマークを角丸カードに収め、01と同じ複製トラックで流し、実際のランディングページでそのまま使える配置です。複製した5枚のカードにはaria-hidden="true"を付け、スクリーンリーダーが同じロゴ名を二度読み上げないようにしています。

로고 카드gap 고정폭linear infinite
.marquee__track {
  display: flex;
  animation: logo-scroll 12s linear infinite;
}
.marquee__logo {
  margin-right: 16px;
  height: 34px;
  padding: 0 16px;
  border-radius: 999px;
  background: #fff;
}

03ホバーで停止

カーソルが帯の上に乗った瞬間、animation-play-stateをpausedへ切り替え、それまで読んでいた行の位置で止めます。実際の本番速度は10秒ですが、グリッドのデモはマウスを動かせないので、トラック自身のkeyframesの中に一時停止の区間を作って2秒ごとに再現しています。

animation-play-statepausedhover
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
  animation-play-state: paused;
}

@keyframes marquee-hover-cycle {
  0%   { transform: translateX(0); }
  38%  { transform: translateX(-32%); }
  62%  { transform: translateX(-32%); }
  100% { transform: translateX(-50%); }
}

04端をマスクでフェード

帯を包む要素にmask-imageの線形グラデーションをかけ、両端の要素が四角く切れる代わりに背景へ溶けるように薄れて見えるようにしました。接頭辞なしのmask-imageはSafari 15.4以降・Chrome 120以降で使えますが、caniuseのmask対応表にあるとおりそれより前のSafariには-webkit-mask-imageも並べて書く必要があります。

mask-imagelinear-gradient-webkit-mask
.marquee {
  overflow: hidden;
  mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
  -webkit-mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
}

05縦ティッカー

translateXの代わりにtranslateYを使い、同じ2セット複製の公式を縦方向に流用しました。AAPL・GOOG・MSFT・NVDAの4銘柄をひとつのリストにまとめ、高さを固定した箱の中で上へ流れ続けるようにしています。

translateY(-50%)column 트랙고정 높이
.ticker__track {
  display: flex;
  flex-direction: column;
  animation: ticker-scroll 12s linear infinite;
}
@keyframes ticker-scroll {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

06双方向の流れ

上下2本のトラックに同じmarquee-scrollのkeyframesを割り当て、下側の行だけanimation-directionをreverseにして流れる向きを交差させます。新しいkeyframesを別に書く必要がないぶん、コードの量が半分で済みます。

animation-direction: reverse두 트랙교차 흐름
.marquee__track {
  animation-name: marquee-scroll;
  animation-duration: 12s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.marquee__row--rev .marquee__track {
  animation-direction: reverse;
}

07速度に緩急をつける流れ

同じ-50%移動でも、タイミング関数をlinearからcubic-bezier(.65,0,.35,1)に差し替えるだけで、継ぎ目の付近はゆっくり、区間の途中は速いという緩急が生まれます。コンベアベルトのような単調さを避けたいロゴ帯に向いています。

cubic-bezier비선형 easing이음매 유지
.marquee__track {
  animation: marquee-scroll 9s
    cubic-bezier(.65, 0, .35, 1) infinite;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

083Dカーブマーキー

舞台にperspective、トラックにtransform-style: preserve-3dをかけたうえで、カード1枚ごとにrotateYとtranslateZの角度を段階的に振り、円筒の壁を横から見ているような並びを作りました。複製した6〜10枚目のカードにも1〜5枚目と同じ角度表を繰り返し使うことで、継ぎ目でも曲率が途切れません。

perspectivepreserve-3d단계별 rotateY
.stage { perspective: 420px; }
.marquee__track { transform-style: preserve-3d; }
.marquee__card:nth-child(1),
.marquee__card:nth-child(6) {
  transform: rotateY(-44deg) translateZ(-40px);
}
.marquee__card:nth-child(3),
.marquee__card:nth-child(8) {
  transform: rotateY(0deg) translateZ(0px);
}

09モーション減時の静止代替

「モーションを減らす」のトグルをオンにすると、トラックのアニメーションはその時点のtransformで止まり、状態表示も「再生中」から「停止中」へ切り替わります。これは実際にprefers-reduced-motion: reduceを有効にした人のブラウザに出る画面と同じ静止状態を、グリッドの中で再現したものです。

prefers-reduced-motion정적 목록토글 비교
@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none !important;
    transform: translateX(-30%);
  }
  .panel__toggle-knob {
    transform: translateX(16px);
  }
}

どこで崩れるか — 落とし穴

02のロゴ帯を最初に組んだとき、複製した5枚のspanにaria-hiddenを付け忘れていました。スクリーンリーダーで読み上げさせるとNORTH・ACME・ORBITなど5つの名前がそのまま二度読み上げられ、MDNのaria-hidden属性の説明にあるとおりこの属性は見た目を変えずに支援技術からだけ要素を隠す働きをするため、複製側のspanひとつひとつにaria-hidden="true"を書き足すと読み上げは5社ぶんだけで止まるようになりました。

06の双方向の流れを作っていたときは、最初はトラックにflexのgapで間隔を付けていました。ところが複製した12枚の間にはgapが11回分しか入らないため、-50%移動ではgapの半分だけ足りず、ループが一周するたびに帯がわずかに跳ねて見えました。Chromeで実測するとこの跳ねはこの記事の9パターン全部で4〜20pxにのぼり、06では5.8pxでした。gapのままtranslateXをtranslateX(calc(-50% - gap / 2))に変えて打ち消す手もありますが、ここでは9パターンすべてでgapをやめ、チップ側にmargin-rightを付ける形に統一しました。margin-rightなら複製した1セット分の幅がトラック全体のちょうど半分になるため、跳ねは9パターンとも0〜0.2pxに収まりました。

この9つのファイルを実際にレンダーし、複製した継ぎ目のずれまで確かめたうえで固めた圧縮ファイルは、本文中にちょうど一度だけ出てくるgcjydr57という文字列を打ち込むと開きます。

アクセシビリティ

9つともprefers-reduced-motion: reduceではtransitionではなくanimationそのものが止まりますが、要素が消えるわけではありません。01・02・04・06・07・08は帯が最初の位置で止まり、05の縦ティッカーも同じ規則で流れを止めて銘柄名が読める状態に落ち着きます。03はhoverとfocus-withinでも同じ一時停止がかかるため、Tabキーで帯に入ったキーボード利用者も読んでいた文言を見失いません。09はさらに一歩進めて、実際にこの設定をオンにしたときの静止画面をトグルで事前に見比べられるようにしました。MDNのprefers-reduced-motionの解説には、この設定が何を伝えるためのものかがまとまっています。

マーキー以外のCSSアニメーションはCSS記事の一覧にまとめてあり、このサイトの全体像はこのサイトについてで紹介しています。

FAQ

なぜ複製はちょうど2セットなのですか。3セット以上にしてはいけませんか

2セットなら移動量を-50%という覚えやすい値にできるからです。3セット複製すれば理屈のうえでは-33.333%、4セットなら-25%でも同じように継ぎ目のないループになりますが、値が複雑になるわりに見た目の差はほとんどなく、この記事の9つは全部2セットの-50%で統一しています。

09のトグルを動かすと実際にOSの設定が変わるのですか

変わりません。09はグリッドの中で自動的にオン・オフを繰り返す見本で、実際にprefers-reduced-motion: reduceを有効にする操作はOSや各ブラウザの設定画面から行います。トグルはその設定をオンにした人に本番でどんな画面が出ているかを、あらかじめ見比べられるように再現しただけです。

タッチ画面では03のホバー停止はどう動きますか

ホバーできる入力装置がない端末では:hoverの条件が成立しないため、帯は止まらずに流れ続けます。指で帯をタップしてフォーカスを当てると、代わりに:focus-withinの条件が成立して同じ一時停止がかかるので、指を離すまでその位置で読めます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。