06후기 슬라이더
고객 후기 카드가 한 장씩 페이드로 바뀌며 저절로 순환합니다. 글자 길이가 서로 달라도 카드들이 같은 자리에 겹쳐 있어 높이가 흔들리지 않습니다.
쓰임후기·리뷰 섹션, 소셜 프루프 배너
opacitygrid 겹침@keyframes

<main class="stage">
<div class="ts">
<div class="ts__stack">
<figure class="ts__card ts__card--1">
<span class="ts__avatar">민</span>
<blockquote class="ts__quote">"배송이 다음 날 바로 왔어요. 포장도 꼼꼼했습니다."</blockquote>
<figcaption class="ts__name">김민지</figcaption>
</figure>
<figure class="ts__card ts__card--2">
<span class="ts__avatar">서</span>
<blockquote class="ts__quote">"색상이 사진이랑 똑같아서 놀랐어요. 재구매 예정."</blockquote>
<figcaption class="ts__name">이서준</figcaption>
</figure>
<figure class="ts__card ts__card--3">
<span class="ts__avatar">하</span>
<blockquote class="ts__quote">"고객센터 응대가 빠르고 친절해서 믿음이 갑니다."</blockquote>
<figcaption class="ts__name">박하은</figcaption>
</figure>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 카드 한 장이 머무는 박자의 기준
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-paper);
.ts { width: min(340px, 84vw); }
// ── 실제 동작: 카드 3장을 같은 grid 칸에 겹치고 opacity 만 @keyframes 로 순환 — 0줄 JS ──
.ts__stack {
position: relative;
display: grid;
border-radius: $r-card;
background: $subject-ink;
padding: $sp-4 $sp-5;
min-height: 128px;
}
.ts__card {
grid-area: 1 / 1;
margin: 0;
opacity: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: $sp-2;
}
.ts__avatar {
width: $sp-8; height: $sp-8;
border-radius: 50%;
display: grid; place-items: center;
background: $color;
color: $subject-cream;
font-weight: 700;
font-size: 14px;
line-height: 1;
font-family: inherit;
}
.ts__quote {
margin: 0;
color: $subject-cream;
font-weight: 400;
font-size: clamp(13px, 3.6vw, 15px);
line-height: 1.5;
font-family: inherit;
}
.ts__name {
color: rgba(255, 247, 230, .6);
font-weight: 600;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
}
.ts__card--1 { animation: ts-1 $duration $easing infinite; }
.ts__card--2 { animation: ts-2 $duration $easing infinite; }
.ts__card--3 { animation: ts-3 $duration $easing infinite; }
// 교차 페이드 창을 12% 로 넓게 — 마지막→처음 순간에 배경이 뚫리는 팝 없이
// 3번의 페이드아웃(88→100%)과 1번의 페이드인(0→12%)이 릴레이로 이어진다 (텔레포트 금지)
@keyframes ts-1 { 0% { opacity: 0; } 12%, 25% { opacity: 1; } 37%, 100% { opacity: 0; } }
@keyframes ts-2 { 0%, 25% { opacity: 0; } 37%, 58% { opacity: 1; } 70%, 100% { opacity: 0; } }
@keyframes ts-3 { 0%, 58% { opacity: 0; } 70%, 88% { opacity: 1; } 100% { opacity: 0; } }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.ts__card { animation: none !important; }
.ts__card--1 { opacity: 1; }
.ts__card--2, .ts__card--3 { opacity: 0; }
}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);
}
.ts {
width: min(340px, 84vw);
}
.ts__stack {
position: relative;
display: grid;
border-radius: 16px;
background: #17141a;
padding: 16px 20px;
min-height: 128px;
}
.ts__card {
grid-area: 1/1;
margin: 0;
opacity: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.ts__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: grid;
place-items: center;
background: #2f6df6;
color: #fff7e6;
font-weight: 700;
font-size: 14px;
line-height: 1;
font-family: inherit;
}
.ts__quote {
margin: 0;
color: #fff7e6;
font-weight: 400;
font-size: clamp(13px, 3.6vw, 15px);
line-height: 1.5;
font-family: inherit;
}
.ts__name {
color: rgba(255, 247, 230, 0.6);
font-weight: 600;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
}
.ts__card--1 {
animation: ts-1 2s ease-out infinite;
}
.ts__card--2 {
animation: ts-2 2s ease-out infinite;
}
.ts__card--3 {
animation: ts-3 2s ease-out infinite;
}
@keyframes ts-1 {
0% {
opacity: 0;
}
12%, 25% {
opacity: 1;
}
37%, 100% {
opacity: 0;
}
}
@keyframes ts-2 {
0%, 25% {
opacity: 0;
}
37%, 58% {
opacity: 1;
}
70%, 100% {
opacity: 0;
}
}
@keyframes ts-3 {
0%, 58% {
opacity: 0;
}
70%, 88% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.ts__card {
animation: none !important;
}
.ts__card--1 {
opacity: 1;
}
.ts__card--2, .ts__card--3 {
opacity: 0;
}
}import type { CSSProperties } from "react";
import styles from "./TestimonialSlider.module.scss";
const REVIEWS = [
{ avatar: "민", quote: "배송이 다음 날 바로 왔어요. 포장도 꼼꼼했습니다.", name: "김민지" },
{ avatar: "서", quote: "색상이 사진이랑 똑같아서 놀랐어요. 재구매 예정.", name: "이서준" },
{ avatar: "하", quote: "고객센터 응대가 빠르고 친절해서 믿음이 갑니다.", name: "박하은" },
];
export type TestimonialSliderProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TestimonialSlider({ duration = 2000, easing = "ease-out", color = "#2f6df6" }: TestimonialSliderProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.ts} style={vars}>
{/* 카드 3장이 같은 grid 칸에 겹쳐 opacity 로만 순환 — 0줄 JS */}
<div className={styles.stack}>
{REVIEWS.map((r, i) => (
<figure key={r.name} className={`${styles.card} ${styles["card" + (i + 1)]}`}>
<span className={styles.avatar}>{r.avatar}</span>
<blockquote className={styles.quote}>"{r.quote}"</blockquote>
<figcaption className={styles.name}>{r.name}</figcaption>
</figure>
))}
</div>
</div>
);
}06 후기 슬라이더 — 측정 누적면적 3.304% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 이미지 슬라이드 만들기 9종 — CSS 캐러셀 그대로 복사
이 데모가 속한 글
이미지 슬라이드 만들기 9종 — CSS 캐러셀 그대로 복사이미지 슬라이드 만들기는 쇼핑몰이나 포트폴리오 사이트에서 사진 몇 장이 옆으로 넘어가며 순서대로 보이는 화면을 만드는 일입니다. AOS·GSAP 없이, JS 0줄로 CSS만 붙여 넣어도 아홉 가지 방식으로 만들 수 이 글의 데모 전부 보기


