05로고 띠 마퀴
도형으로 만든 마크 여덟 개가 한 줄로 끝없이 흘러가고, 띠의 양 끝은 무대 색으로 흐려져 마크가 갑자기 잘리지 않습니다.
쓰임도입 고객사 띠 · 파트너 소개 줄
translateXmargin-rightmask-image

<main class="stage">
<section class="lm is-demo" aria-label="도입 고객사 띠">
<p class="lm__lead">500곳이 넘는 팀이 쓰고 있습니다</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>
온라인 서점
</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>
사진 스튜디오
</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>
물류 회사
</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>
디자인 에이전시
</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>
어학원
</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>
동네 빵집
</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>
여행 플랫폼
</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>
피트니스 센터
</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>
온라인 서점
</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>
사진 스튜디오
</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>
물류 회사
</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>
디자인 에이전시
</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>
어학원
</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>
동네 빵집
</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>
여행 플랫폼
</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>
피트니스 센터
</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 로고 띠 마퀴 — 측정 누적면적 4.272% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 고객 후기 디자인 패턴 9종, 신뢰를 보여 주는
이 데모가 속한 글
고객 후기 디자인 패턴 9종, 신뢰를 보여 주는고객 후기 디자인은 다른 사람이 남긴 평가를 화면에 얹어, 망설이는 방문자를 마지막으로 안심시키는 섹션입니다. 아홉 가지는 후기 카드·별점·고객사 목록을 HTML 과 CSS 만으로 지어서 옮겨 붙이면 그대로 움직입니 이 글의 데모 전부 보기


