04후기 캐러셀 — 자동 진행 값이 타이머로 늘어나는 카드 줄
후기 카드는 자동 진행 값이 일정 시간마다 늘어나며 다음 카드로 위치가 보간되어 넘어가고, 포인터가 올라간 상태가 되면 그 값의 증가가 멈춥니다.
쓰임고객 후기 섹션, 사용자 사례 슬라이더
setIntervaltranslateXclearInterval

<main class="stage" id="stage">
<div class="carousel" id="carousel">
<div class="track is-demo" id="track">
<blockquote class="t-card">
<p class="t-card__quote">설치가 정말 쉬웠어요</p>
<p class="t-card__name">김민지</p>
</blockquote>
<blockquote class="t-card">
<p class="t-card__quote">고객 지원이 빨라요</p>
<p class="t-card__name">박서준</p>
</blockquote>
<blockquote class="t-card">
<p class="t-card__quote">디자인이 깔끔해요</p>
<p class="t-card__name">이하늘</p>
</blockquote>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-blue-deep;
@include stage($stage-paper);
.carousel {
width: 100%;
max-width: 320px;
overflow: hidden;
border-radius: $r-card;
}
.track {
display: flex;
width: 100%;
}
.t-card {
box-sizing: border-box;
flex: 0 0 100%;
margin: 0;
padding: $sp-6 $sp-4;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
background: $color;
text-align: center;
}
.t-card__quote {
margin: 0;
color: $stage-paper;
font-weight: 600;
font-size: 13px;
line-height: 1.5;
}
.t-card__quote::before { content: "\201C"; }
.t-card__quote::after { content: "\201D"; }
.t-card__name {
margin: 0;
color: rgba($stage-paper, .78);
font-weight: 400;
font-size: 11px;
line-height: 1.5;
}
.track.is-demo {
animation: advance $duration $easing infinite;
}
@keyframes advance {
0%, 20% { transform: translateX(0); }
33%, 63% { transform: translateX(-100%); }
76%, 96% { transform: translateX(-200%); }
100% { transform: translateX(0); }
}
@media (max-width: 340px) {
.carousel { max-width: 240px; }
.t-card { padding: $sp-4 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
.track.is-demo { animation: none; transform: none; }
}@charset "UTF-8";
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);
}
.carousel {
width: 100%;
max-width: 320px;
overflow: hidden;
border-radius: 16px;
}
.track {
display: flex;
width: 100%;
}
.t-card {
box-sizing: border-box;
flex: 0 0 100%;
margin: 0;
padding: 24px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
background: #0a4fe8;
text-align: center;
}
.t-card__quote {
margin: 0;
color: #ffffff;
font-weight: 600;
font-size: 13px;
line-height: 1.5;
}
.t-card__quote::before {
content: "“";
}
.t-card__quote::after {
content: "”";
}
.t-card__name {
margin: 0;
color: rgba(255, 255, 255, 0.78);
font-weight: 400;
font-size: 11px;
line-height: 1.5;
}
.track.is-demo {
animation: advance 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes advance {
0%, 20% {
transform: translateX(0);
}
33%, 63% {
transform: translateX(-100%);
}
76%, 96% {
transform: translateX(-200%);
}
100% {
transform: translateX(0);
}
}
@media (max-width: 340px) {
.carousel {
max-width: 240px;
}
.t-card {
padding: 16px 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.track.is-demo {
animation: none;
transform: none;
}
}// 자동 진행 값이 타이머로 계속 늘어나며 카드 위치를 다음 카드로 보간한다. 포인터가 올라와 있는 동안은 그 값이 늘지 않는다.
// 계산 겹: 자동 진행 값(interval) -> translateX 보간. 실사용 포인터 리스너 있음(데모 재생 자체는 고정 시퀀스).
(function () {
var carousel = document.getElementById('carousel');
var track = document.getElementById('track');
if (!carousel || !track) return;
var count = track.children.length;
var index = 0;
var timer = null;
var live = false;
function render() {
track.style.transform = 'translateX(' + (index * -100) + '%)';
}
function advance() {
index = (index + 1) % count;
render();
}
function stop() {
if (timer !== null) { window.clearInterval(timer); timer = null; }
}
function start() {
stop();
timer = window.setInterval(advance, 2600);
}
function goLive() {
if (live) return;
live = true;
track.classList.remove('is-demo');
render();
start();
}
carousel.addEventListener('pointerenter', function () {
goLive();
stop();
});
carousel.addEventListener('pointerleave', function () {
if (live) start();
});
})();import { useEffect, useRef } from "react";
import styles from "./TestimonialCarousel.module.scss";
// 자동 진행 값이 타이머로 계속 늘어나며 카드 위치를 다음 카드로 보간한다. 포인터가 올라와 있는 동안은 그 값이 늘지 않는다.
// 계산 겹: 자동 진행 값(interval) -> translateX 보간. 실사용 포인터 리스너 있음(데모 재생 자체는 고정 시퀀스).
const REVIEWS = [
{ quote: "설치가 정말 쉬웠어요", name: "김민지" },
{ quote: "고객 지원이 빨라요", name: "박서준" },
{ quote: "디자인이 깔끔해요", name: "이하늘" },
];
export default function TestimonialCarousel() {
const carouselRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const carousel = carouselRef.current;
const track = trackRef.current;
if (!carousel || !track) return;
const count = track.children.length;
let index = 0;
let timer: number | null = null;
let live = false;
function render() {
track!.style.transform = "translateX(" + index * -100 + "%)";
}
function advance() {
index = (index + 1) % count;
render();
}
function stop() {
if (timer !== null) {
window.clearInterval(timer);
timer = null;
}
}
function start() {
stop();
timer = window.setInterval(advance, 2600);
}
function goLive() {
if (live) return;
live = true;
track!.classList.remove(styles["is-demo"]);
render();
start();
}
function onEnter() {
goLive();
stop();
}
function onLeave() {
if (live) start();
}
carousel.addEventListener("pointerenter", onEnter);
carousel.addEventListener("pointerleave", onLeave);
return () => {
stop();
carousel.removeEventListener("pointerenter", onEnter);
carousel.removeEventListener("pointerleave", onLeave);
};
}, []);
return (
<main className={styles.stage}>
<div ref={carouselRef} className={styles.carousel}>
<div ref={trackRef} className={`${styles.track} ${styles["is-demo"]}`}>
{REVIEWS.map((r) => (
<blockquote key={r.name} className={styles["t-card"]}>
<p className={styles["t-card__quote"]}>{r.quote}</p>
<p className={styles["t-card__name"]}>{r.name}</p>
</blockquote>
))}
</div>
</div>
</main>
);
}04 후기 캐러셀 — 자동 진행 값이 타이머로 늘어나는 카드 줄 — 측정 누적면적 2.61% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 랜딩페이지 히어로섹션 디자인 9종 — 통계 카운터까지
이 데모가 속한 글
랜딩페이지 히어로섹션 디자인 9종 — 통계 카운터까지랜딩페이지 히어로섹션 아홉 개 중 하나는 카드가 보이는 비율 값이 기준을 넘을 때마다 순서 값에 간격을 곱해 나타나는 시점을 다시 구하고, 하나는 열린 질문의 답변 높이 값을 내용 길이로 다시 구해 펼치며, 또 하나 이 글의 데모 전부 보기


