08Logo bar — a marquee that recomputes speed from logo count to keep loop time constant
Counting the logos works out a scroll speed proportional to that count so one full loop always takes the same amount of time, whether there are few logos or many.
Best forClient logo marquees, partner trust bars on case pages
getBoundingClientRecttranslateX--marquee-duration

<main class="stage">
<section class="panel">
<p class="hint" id="hint">Even if the logo count changes, the scroll speed (pixels per second) stays the same — it measures the actual rendered width and recalculates the loop duration every time.</p>
<div class="marqueeWrap">
<div class="marqueeTrack" id="track">
<span class="logo" data-set="a">NOVA</span>
<span class="logo" data-set="a">ATLAS</span>
<span class="logo" data-set="a">COBALT</span>
<span class="logo" data-set="a">SILVERLINE</span>
<span class="logo" data-set="a">TERRA</span>
<span class="logo" data-set="a">PRISM</span>
<span class="logo" data-set="b" aria-hidden="true">NOVA</span>
<span class="logo" data-set="b" aria-hidden="true">ATLAS</span>
<span class="logo" data-set="b" aria-hidden="true">COBALT</span>
<span class="logo" data-set="b" aria-hidden="true">SILVERLINE</span>
<span class="logo" data-set="b" aria-hidden="true">TERRA</span>
<span class="logo" data-set="b" aria-hidden="true">PRISM</span>
</div>
</div>
<p class="speedNote" id="speedNote">Calculated one loop: 0.0s</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: linear;
$color: $subject-ink;
@include stage($stage-paper);
.panel {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: $sp-2;
}
.hint {
margin: 0;
text-align: center;
color: rgba($subject-ink, .72);
font-weight: 400;
font-size: 10px;
line-height: 1.35;
}
.marqueeWrap {
overflow: hidden;
border-radius: $r-control;
background: rgba($subject-cream, .55);
padding: $sp-3 0;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marqueeTrack {
display: flex;
width: max-content;
--marquee-duration: #{$dur-loop};
animation: marquee var(--marquee-duration) $easing infinite;
}
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.logo {
flex: 0 0 auto;
margin-right: $sp-6;
color: $color;
font-weight: 700;
font-size: 15px;
letter-spacing: .04em;
white-space: nowrap;
}
.speedNote {
margin: 0;
text-align: center;
color: rgba($subject-ink, .72);
font-weight: 500;
font-size: 9px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.panel { max-width: 280px; gap: $sp-1; }
.logo { font-size: 13px; margin-right: $sp-4; }
.hint { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.marqueeTrack {
animation-play-state: paused;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.panel {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 8px;
}
.hint {
margin: 0;
text-align: center;
color: rgba(23, 20, 26, 0.72);
font-weight: 400;
font-size: 10px;
line-height: 1.35;
}
.marqueeWrap {
overflow: hidden;
border-radius: 12px;
background: rgba(255, 247, 230, 0.55);
padding: 12px 0;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marqueeTrack {
display: flex;
width: max-content;
--marquee-duration: 2s;
animation: marquee var(--marquee-duration) linear infinite;
}
@keyframes marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.logo {
flex: 0 0 auto;
margin-right: 24px;
color: #17141a;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.04em;
white-space: nowrap;
}
.speedNote {
margin: 0;
text-align: center;
color: rgba(23, 20, 26, 0.72);
font-weight: 500;
font-size: 9px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.panel {
max-width: 280px;
gap: 4px;
}
.logo {
font-size: 13px;
margin-right: 16px;
}
.hint {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.marqueeTrack {
animation-play-state: paused;
}
}// Logos: 바(08) — Logos: 개수가 바뀌어도 체감 속도(초당 픽셀)가 같도록, 실제 렌더 폭을 재서
// 한 바퀴 시간을 매번 계산한다(계산 한 겹). 트랙을 통째로 2벌 두고 -50% 만큼 옮기는
// 무한 루프라 flex gap 대신 margin-right 로 간격을 준다(BD15).
(function () {
var track = document.getElementById('track');
var speedNote = document.getElementById('speedNote');
if (!track || !speedNote) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var LOOP_SEC = 15; // 변형: 15 → 30 (기준 폭에서의 한 바퀴 시간)
var BASE_WIDTH = 560; // 기준 렌더 폭(px) — 이 폭일 때 LOOP_SEC 이 그대로 적용된다
var MIN_SEC = 8;
var MAX_SEC = 40;
function recompute() {
var setA = track.querySelectorAll('.logo[data-set="a"]');
var halfWidth = 0;
setA.forEach(function (el) {
var rect = el.getBoundingClientRect();
var style = getComputedStyle(el);
halfWidth += rect.width + parseFloat(style.marginRight || '0');
});
var duration = LOOP_SEC * (halfWidth / BASE_WIDTH);
duration = Math.max(MIN_SEC, Math.min(MAX_SEC, duration));
track.style.setProperty('--marquee-duration', duration.toFixed(2) + 's');
var pxPerSec = halfWidth / duration;
speedNote.textContent = 'Logos: ' + setA.length + ' · calculated loop ' + duration.toFixed(1) + 's · ' + Math.round(pxPerSec) + 'px';
}
recompute();
window.addEventListener('resize', recompute);
if (reduce) {
track.style.animationPlayState = 'paused';
}
})();// CaseLogoBar — vanilla 판(run/340/demo/08-case-logo-bar/index.html) 의 React 포트.
// 로고 개수가 바뀌어도 체감 속도(초당 픽셀)가 같도록, 실제 렌더 폭을 재서 한 바퀴 시간을
// 매번 계산한다. 트랙을 통째로 2벌 두고 -50% 만큼 옮기는 무한 루프라 flex gap 대신
// margin-right 로 간격을 준다(BD15).
import { useEffect, useRef, useState } from "react";
import styles from "./CaseLogoBar.module.scss";
export type CaseLogoBarProps = {
duration?: string;
easing?: string;
color?: string;
};
const HINT = "로고 개수가 바뀌어도 흐르는 속도(초당 픽셀)는 같도록, 실제 렌더 폭을 재서 한 바퀴 시간을 매번 계산합니다";
const LOGOS = ["NOVA", "ATLAS", "COBALT", "SILVERLINE", "TERRA", "PRISM"];
const LOOP_SEC = 15; // 변형: 15 → 30 (기준 폭에서의 한 바퀴 시간)
const BASE_WIDTH = 560; // 기준 렌더 폭(px) — 이 폭일 때 LOOP_SEC 이 그대로 적용된다
const MIN_SEC = 8;
const MAX_SEC = 40;
export default function CaseLogoBar({
duration = "2s",
easing = "linear",
color = "#17141a"
}: CaseLogoBarProps) {
const trackRef = useRef<HTMLDivElement | null>(null);
const [speedNote, setSpeedNote] = useState("계산된 한 바퀴 0.0초");
useEffect(() => {
const track = trackRef.current;
if (!track) return;
function recompute(): void {
if (!track) return;
const setA = track.querySelectorAll<HTMLElement>('[data-set="a"]');
let halfWidth = 0;
setA.forEach((el) => {
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
halfWidth += rect.width + parseFloat(style.marginRight || "0");
});
let dur = LOOP_SEC * (halfWidth / BASE_WIDTH);
dur = Math.max(MIN_SEC, Math.min(MAX_SEC, dur));
track.style.setProperty("--marquee-duration", dur.toFixed(2) + "s");
const pxPerSec = halfWidth / dur;
setSpeedNote(
"로고 " + setA.length + "개 · 계산된 한 바퀴 " + dur.toFixed(1) + "초 · 초당 " + Math.round(pxPerSec) + "px"
);
}
recompute();
window.addEventListener("resize", recompute);
return () => window.removeEventListener("resize", recompute);
}, []);
return (
<main className={styles.stage}>
<section className={styles.panel}>
<p className={styles.hint}>{HINT}</p>
<div className={styles.marqueeWrap}>
<div className={styles.marqueeTrack} ref={trackRef}>
{LOGOS.map((name) => (
<span key={"a-" + name} className={styles.logo} data-set="a">
{name}
</span>
))}
{LOGOS.map((name) => (
<span key={"b-" + name} className={styles.logo} data-set="b" aria-hidden="true">
{name}
</span>
))}
</div>
</div>
<p className={styles.speedNote}>{speedNote}</p>
</section>
</main>
);
}08 Logo bar — a marquee that recomputes speed from logo count to keep loop time constant — Measured area 3.005% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 Case Study Page Design Sections (Copy-Paste)
The post this demo lives in
9 Case Study Page Design Sections (Copy-Paste)Nine case-study sections that compute: the hero recounts its metric frame by frame, tiles size from a computed impact sc See all demos in this post
If you like this effect - similar demos
Landing sections06 Partner logo bar — a scroll that reverse-computes its speed from the measured track width9 Team Page Design Patterns (Copy-Paste)
Landing sections02 Project grid9 Project Showcase Section Templates — Copy-Paste
Landing sections05 Logo marquee bandTestimonial Section Design: 9 Social Proof Blocks