web design
COMPONENT U / SEAMLESS MARQUEE · LOOP + HOVER PAUSE
GUIDE / SEAMLESS MARQUEE

Overview

A seamless infinite marquee. The track is rendered twice and translated negative fifty percent, so the loop point is invisible. It pauses on hover and the duplicate track is hidden from assistive tech. Edge masking fades the first and last seven percent into transparency so entries and exits read as dissolves, not cuts.

When to use

A row of partner or integration names. Each item pairs a mark with its name; in the source the logos are forced to a single tone via a brightness and invert filter so any source colour resolves to the theme.

Spec

Tokens
--muted names, --gold dot, --line rules
Motion
translateX 0 to -50%, 42s linear infinite
A11y
duplicate track aria-hidden; paused on hover
Layout
track rendered twice; edge mask 7%

Anti-patterns

Never render the track once; the negative-fifty-percent loop needs two copies. Do not ease the animation; linear is what makes it seamless. Do not drop the edge mask or the loop point shows a hard name boundary.

<style>
:root{
  --bg:#0b0a08;--surface:#14110b;--ivory:#efe8d6;--muted:#9c9078;--faint:#5a5446;
  --line:#2a2519;--line-faint:rgba(239,232,214,.08);--gold:#c9a24b;--gold-soft:#e7cd8c;
  --font-display:'Instrument Serif',ui-serif,Georgia,'Times New Roman',serif;
  --font-title:'Libre Caslon Condensed',ui-serif,Georgia,'Times New Roman',serif;
  --font-body:'Inter',system-ui,-apple-system,sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --ease:cubic-bezier(.22,1,.36,1);--dur-fast:200ms;--dur-base:400ms;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--ivory);font-family:var(--font-body);min-height:100vh;padding:64px 48px}
@keyframes hf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.animate-marquee{animation:hf-marquee var(--marquee-duration,38s) linear infinite}
.marquee-paused:hover .animate-marquee{animation-play-state:paused}
.mq{position:relative;overflow:hidden;max-width:1200px;margin:0 auto;-webkit-mask-image:linear-gradient(to right,transparent,black 7%,black 93%,transparent);mask-image:linear-gradient(to right,transparent,black 7%,black 93%,transparent);padding:24px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.mq-track{display:flex;width:max-content;will-change:transform}
.mq ul{display:flex;align-items:center;gap:56px;padding-right:56px;list-style:none;flex-shrink:0}
.mq li{display:flex;align-items:center;gap:12px;color:var(--muted);flex-shrink:0}
.mq .dot{width:6px;height:6px;border-radius:9999px;background:rgba(201,162,75,.5);flex-shrink:0}
.mq .name{font-family:var(--font-title);font-size:1.5rem;font-weight:400;letter-spacing:.01em;white-space:nowrap}
@media(min-width:768px){.mq .name{font-size:2rem}}
@media(prefers-reduced-motion:reduce){.animate-marquee{animation:none}}
</style>

<div class="mq marquee-paused">
  <div class="mq-track animate-marquee" id="track" style="--marquee-duration:42s"></div>
</div>

<script>
// Partner names; source pairs each with a logo forced to a single tone.
// Logos live in the site's /public/logos and are not bundled here, so each
// name is set with a small gold dot in front, which still demonstrates the loop.
var PARTNERS = ["NinjaTrader","Quantower","HyperLiquid","Databento","Nado","Interactive Brokers","MetaTrader 5","Darwinex"];
function buildList(hidden){
  var ul = document.createElement("ul");
  if(hidden) ul.setAttribute("aria-hidden","true");
  PARTNERS.forEach(function(name){
    var li = document.createElement("li");
    var dot = document.createElement("span"); dot.className = "dot"; dot.setAttribute("aria-hidden","true");
    var n = document.createElement("span"); n.className = "name"; n.textContent = name;
    li.appendChild(dot); li.appendChild(n);
    ul.appendChild(li);
  });
  return ul;
}
var track = document.getElementById("track");
track.appendChild(buildList(false));
track.appendChild(buildList(true));
</script>