05Logo marquee band
Eight client marks — a shape and a plain-word name — drift past in one endless line, and both ends of the band fade into the stage color so no mark is cut off abruptly.
Best forCustomer logo bands and partner strips
translateXmargin-rightmask-image

<main class="stage">
<section class="lm is-demo" aria-label="Customer logo band">
<p class="lm__lead">Trusted by more than 500 teams</p>
<div class="lm__band">
<!-- 마크 8개를 두 벌 이어 붙인 트랙. 간격을 flex gap 으로 주면 16개 사이 간격이 15개라
translateX(-50%) 가 한 간격만큼 모자라 한 바퀴마다 튄다 (BD15).
간격을 항목마다 margin-right 로 주면 트랙 폭 = (항목폭 + 간격) × 16 이라 절반이 정확히 한 벌이다 -->
<div class="lm__track">
<a class="lm__mark lm__mark--t1" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/></svg>
Online Bookshop
</a>
<a class="lm__mark lm__mark--t2" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 3 21 20H3Z"/></svg>
Photo Studio
</a>
<a class="lm__mark lm__mark--t3" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 22 12 12 22 2 12Z"/></svg>
Logistics Co.
</a>
<a class="lm__mark lm__mark--t4" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 21 7v10l-9 5-9-5V7Z"/></svg>
Design Agency
</a>
<a class="lm__mark lm__mark--t1" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="18" rx="5"/></svg>
Language School
</a>
<a class="lm__mark lm__mark--t2" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="4"/></svg>
Corner Bakery
</a>
<a class="lm__mark lm__mark--t3" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 22 9.3 18.2 21H5.8L2 9.3Z"/></svg>
Travel Platform
</a>
<a class="lm__mark lm__mark--t4" href="#">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7Z"/></svg>
Fitness Center
</a>
<a class="lm__mark lm__mark--t1 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/></svg>
Online Bookshop
</a>
<a class="lm__mark lm__mark--t2 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 3 21 20H3Z"/></svg>
Photo Studio
</a>
<a class="lm__mark lm__mark--t3 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 22 12 12 22 2 12Z"/></svg>
Logistics Co.
</a>
<a class="lm__mark lm__mark--t4 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 21 7v10l-9 5-9-5V7Z"/></svg>
Design Agency
</a>
<a class="lm__mark lm__mark--t1 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="18" rx="5"/></svg>
Language School
</a>
<a class="lm__mark lm__mark--t2 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="4"/></svg>
Corner Bakery
</a>
<a class="lm__mark lm__mark--t3 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 22 9.3 18.2 21H5.8L2 9.3Z"/></svg>
Travel Platform
</a>
<a class="lm__mark lm__mark--t4 lm__mark--dup" href="#" tabindex="-1" aria-hidden="true">
<svg class="lm__shape" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7Z"/></svg>
Fitness Center
</a>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 276 공통 팔레트 — 아홉 항목이 같은 값을 쓴다. 괄호 안은 흰 무대 기준 실측 대비비
$pf-ink: #1c1b22; // 한 줄 문구 17.08:1
$pf-paper: $stage-paper; // 무대
$pf-stone: #565062; // 마크 이름 7.72:1
$pf-teal: #479089; // 마크 도형 5.79:1
$pf-violet: #a49cce; // 마크 도형 6.12:1
$pf-flame: $stage-orange; // 마크 도형 3.32:1 (글자가 아니라 도형)
$duration: $dur-loop; // 마크 한 벌이 지나가는 한 바퀴 — 설정 초와 같은 2초
$easing: linear; // 마퀴는 등속이어야 이음매가 안 보인다
$color: $pf-paper; // 무대 · 띠 양 끝이 녹아드는 색
@include stage($pf-paper);
// 시각 요소 reset — 기본 margin 을 명시로 0
p { margin: 0; }
.lm {
box-sizing: border-box;
display: grid;
justify-items: center;
gap: $sp-4;
width: clamp(288px, 96vw, 448px);
}
.lm__lead {
color: $pf-ink;
font-weight: 700; font-size: clamp(13px, 3.4vw, 16px); line-height: 1.3;
text-align: center;
}
// 띠 양 끝을 무대 색으로 흐린다 — 마크가 경계에서 갑자기 잘리지 않는다
.lm__band {
box-sizing: border-box;
width: 100%;
padding-top: $sp-4;
padding-bottom: $sp-4;
border-top: 1px solid rgba(28, 27, 34, .14);
border-bottom: 1px solid rgba(28, 27, 34, .14);
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
}
.lm__track {
display: flex;
align-items: center;
width: max-content;
}
// BD15 — 트랙에는 flex gap 을 주지 않는다. 16개 사이 gap 은 15개뿐이라
// translateX(-50%) 가 gap 절반만큼 모자라 한 바퀴마다 튄다.
// margin-right 는 마지막 항목에도 붙으므로 트랙 폭 = (항목폭 + 24px) × 16,
// 즉 절반이 정확히 마크 한 벌이다
.lm__mark {
display: inline-flex;
align-items: center;
gap: $sp-2;
flex: 0 0 auto;
margin-right: $sp-6;
padding: $sp-1;
border-radius: $r-sm;
color: $pf-stone;
font-weight: 600; font-size: 12px; line-height: 1;
white-space: nowrap;
text-decoration: none;
}
.lm__mark:focus-visible {
outline: 2px solid $pf-ink;
outline-offset: 2px;
}
.lm__shape { display: block; flex: 0 0 auto; }
.lm__mark--t1 .lm__shape { color: $pf-ink; }
.lm__mark--t2 .lm__shape { color: $pf-teal; }
.lm__mark--t3 .lm__shape { color: $pf-flame; }
.lm__mark--t4 .lm__shape { color: $pf-violet; }
// ── 자동 재생 ────────────────────────────────────────────
// animation 단축은 delay 를 0 으로 리셋하므로 낱개 속성으로 쓴다
.lm.is-demo .lm__track {
animation-name: lmRun;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.lm.is-paused .lm__track { animation-play-state: paused; }
@keyframes lmRun {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
// 좁은 화면 — 글자·도형·간격만 줄이고 한 줄 흐름은 그대로
@media (max-width: 340px), (max-height: 220px) {
.lm { gap: $sp-2; width: 100%; }
.lm__lead { font-size: 12px; }
.lm__band { padding-top: $sp-2; padding-bottom: $sp-2; }
.lm__mark { margin-right: $sp-4; padding: 0; font-size: 10px; }
.lm__shape { width: 14px; height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
// .lm.is-demo .lm__track (0,3,0) 를 이기려면 !important 가 필요하다.
// 흐름을 멈추면 한 줄에는 마크 여덟 개가 다 안 들어가므로 두 줄로 접어 전부 보인다
.lm__track {
animation: none !important;
flex-wrap: wrap !important;
justify-content: center !important;
width: 100% !important;
}
// 줄 간격도 gap 이 아니라 항목 margin 으로 준다 — 트랙 규칙에 gap 이 들어가면
// 흐르는 판과 멈춘 판이 같은 이음새 규칙을 안 쓰게 된다
.lm__mark { margin-bottom: $sp-3 !important; }
.lm__band {
-webkit-mask-image: none !important;
mask-image: none !important;
}
.lm__mark--dup { display: none !important; }
}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);
}
p {
margin: 0;
}
.lm {
box-sizing: border-box;
display: grid;
justify-items: center;
gap: 16px;
width: clamp(288px, 96vw, 448px);
}
.lm__lead {
color: #1c1b22;
font-weight: 700;
font-size: clamp(13px, 3.4vw, 16px);
line-height: 1.3;
text-align: center;
}
.lm__band {
box-sizing: border-box;
width: 100%;
padding-top: 16px;
padding-bottom: 16px;
border-top: 1px solid rgba(28, 27, 34, 0.14);
border-bottom: 1px solid rgba(28, 27, 34, 0.14);
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
}
.lm__track {
display: flex;
align-items: center;
width: max-content;
}
.lm__mark {
display: inline-flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
margin-right: 24px;
padding: 4px;
border-radius: 8px;
color: #565062;
font-weight: 600;
font-size: 12px;
line-height: 1;
white-space: nowrap;
text-decoration: none;
}
.lm__mark:focus-visible {
outline: 2px solid #1c1b22;
outline-offset: 2px;
}
.lm__shape {
display: block;
flex: 0 0 auto;
}
.lm__mark--t1 .lm__shape {
color: #1c1b22;
}
.lm__mark--t2 .lm__shape {
color: #479089;
}
.lm__mark--t3 .lm__shape {
color: #ff4d1f;
}
.lm__mark--t4 .lm__shape {
color: #a49cce;
}
.lm.is-demo .lm__track {
animation-name: lmRun;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.lm.is-paused .lm__track {
animation-play-state: paused;
}
@keyframes lmRun {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@media (max-width: 340px), (max-height: 220px) {
.lm {
gap: 8px;
width: 100%;
}
.lm__lead {
font-size: 12px;
}
.lm__band {
padding-top: 8px;
padding-bottom: 8px;
}
.lm__mark {
margin-right: 16px;
padding: 0;
font-size: 10px;
}
.lm__shape {
width: 14px;
height: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.lm__track {
animation: none !important;
flex-wrap: wrap !important;
justify-content: center !important;
width: 100% !important;
}
.lm__mark {
margin-bottom: 12px !important;
}
.lm__band {
-webkit-mask-image: none !important;
mask-image: none !important;
}
.lm__mark--dup {
display: none !important;
}
}(function () {
var root = document.querySelector('.lm');
// 마퀴는 흐름 자체가 부품이라 .is-demo 를 떼지 않는다. 손을 대면 그 자리에 멈춘다
function pause() { root.classList.add('is-paused'); }
function play() { root.classList.remove('is-paused'); }
root.addEventListener('pointerenter', pause);
root.addEventListener('pointerleave', play);
// 터치: 손가락을 대면 멈추고 떼면 다시 흐른다
root.addEventListener('pointerdown', pause);
root.addEventListener('pointerup', play);
root.addEventListener('pointercancel', play);
// 키보드: Tab 으로 마크에 들어오면 멈추고 나가면 다시 흐른다
root.addEventListener('focusin', pause);
root.addEventListener('focusout', play);
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./LogoMarquee.module.scss";
/* ambient 에 React 이벤트 제네릭이 없어 파일 안에 필요한 만큼만 선언한다 */
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export type MarqueeMark = {
/** 일반 명사 이름 — 실재 브랜드 이름은 쓰지 않는다 */
name: string;
/** 일반 도형 인라인 SVG */
shape: ReactNode;
};
export type LogoMarqueeProps = {
sectionLabel?: string;
lead?: string;
marks?: MarqueeMark[];
/** 마크 한 벌이 지나가는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 띠 양 끝이 녹아드는 무대 색 */
color?: string;
};
const Circle = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" /></svg>
);
const Triangle = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 3 21 20H3Z" /></svg>
);
const Diamond = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 22 12 12 22 2 12Z" /></svg>
);
const Hexagon = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 21 7v10l-9 5-9-5V7Z" /></svg>
);
const Rounded = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="18" rx="5" /></svg>
);
const Ring = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="4" /></svg>
);
const Pentagon = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 2 22 9.3 18.2 21H5.8L2 9.3Z" /></svg>
);
const Cross = (): ReactNode => (
<svg className={styles.shape} viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7Z" /></svg>
);
const DEFAULT_MARKS: MarqueeMark[] = [
{ name: "온라인 서점", shape: <Circle /> },
{ name: "사진 스튜디오", shape: <Triangle /> },
{ name: "물류 회사", shape: <Diamond /> },
{ name: "디자인 에이전시", shape: <Hexagon /> },
{ name: "어학원", shape: <Rounded /> },
{ name: "동네 빵집", shape: <Ring /> },
{ name: "여행 플랫폼", shape: <Pentagon /> },
{ name: "피트니스 센터", shape: <Cross /> },
];
export default function LogoMarquee({
sectionLabel = "도입 고객사 띠",
lead = "500곳이 넘는 팀이 쓰고 있습니다",
marks = DEFAULT_MARKS,
duration = 2000,
easing = "linear",
color = "#ffffff",
}: LogoMarqueeProps): ReactNode {
/* 마퀴는 흐름 자체가 부품이라 루프를 끄지 않는다. 손을 대면 그 자리에 멈춘다 */
const [paused, setPaused] = useState<boolean>(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const tone = [styles.t1, styles.t2, styles.t3, styles.t4];
const cls = [styles.lm, styles.isDemo, paused ? styles.isPaused : ""].filter(Boolean).join(" ");
return (
<section
className={cls}
style={vars}
aria-label={sectionLabel}
onPointerEnter={() => setPaused(true)}
onPointerLeave={() => setPaused(false)}
onPointerDown={() => setPaused(true)}
onPointerUp={() => setPaused(false)}
onFocus={() => setPaused(true)}
onBlur={() => setPaused(false)}
>
<p className={styles.lead}>{lead}</p>
<div className={styles.band}>
{/* 마크 8개를 두 벌 이어 붙인 트랙. 간격을 flex gap 으로 주면 16개 사이 간격이 15개라
translateX(-50%) 가 한 간격만큼 모자라 한 바퀴마다 튄다 (BD15).
간격을 항목마다 margin-right 로 주면 트랙 폭 = (항목폭 + 간격) × 16 이라
절반이 정확히 마크 한 벌이다 */}
<div className={styles.track}>
{marks.map((m: MarqueeMark, i: number) => (
<a className={`${styles.mark} ${tone[i % tone.length]}`} href="#" key={`a-${m.name}`}>
{m.shape}
{m.name}
</a>
))}
{marks.map((m: MarqueeMark, i: number) => (
<a
className={`${styles.mark} ${tone[i % tone.length]} ${styles.dup}`}
href="#"
key={`b-${m.name}`}
tabIndex={-1}
aria-hidden="true"
>
{m.shape}
{m.name}
</a>
))}
</div>
</div>
</section>
);
}05 Logo marquee band — Measured area 4.272% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Testimonial Section Design: 9 Social Proof Blocks
The post this demo lives in
Testimonial Section Design: 9 Social Proof BlocksTestimonial section design is the band of a page that shows proof from real customers — quotes, star ratings, client mar See all demos in this post
If you like this effect - similar demos
Landing sections08 Logo bar — a marquee that recomputes speed from logo count to keep loop time constant9 Case Study Page Design Sections (Copy-Paste)
Landing sections02 Logo cloud / trusted-by bar9 Landing Page Sections That Build Trust
Landing sections08 Logo bar — a row whose horizontal offset value keeps growing without a seamHero Section Design Examples: 9 Patterns