01스크롤 리빌 플랜
가격표가 화면에 들어오면 플랜 카드가 순서대로 떠오르며 나타나, 방문자가 카드를 하나씩 훑어보게 만듭니다.
쓰임요금제 소개 랜딩 페이지, 스크롤형 가격 비교 섹션
IntersectionObservertransition-delayJS 7줄

<main class="stage">
<div class="sr-grid is-demo">
<div class="sr-card">
<span class="sr-card__tag">Basic</span>
<span class="sr-card__price">$9</span>
</div>
<div class="sr-card sr-card--mid">
<span class="sr-card__tag">Pro</span>
<span class="sr-card__price">$29</span>
</div>
<div class="sr-card">
<span class="sr-card__tag">Team</span>
<span class="sr-card__price">$79</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-ink);
.sr-grid {
width: min(340px, 90vw);
aspect-ratio: 340 / 190;
display: flex;
align-items: flex-end;
gap: $sp-2;
}
.sr-card {
flex: 1 1 0;
height: 78%;
border-radius: $r-sm;
box-sizing: border-box;
padding: $sp-3;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: $sp-1;
color: $subject-cream;
background: rgba(255, 255, 255, .08);
font: 600 12px/1 -apple-system, "Segoe UI", sans-serif;
}
.sr-card--mid {
height: 100%;
background: $subject-blue-deep; // 크림 글자가 얹히므로 진한 파랑
}
.sr-card__tag { opacity: 1; font-size: 11px; } // AA: 진한 파랑 면 위 크림 100% — 5.98:1
.sr-card__price { font-size: 20px; font-weight: 700; margin-top: $sp-1; }
// 실사용: IntersectionObserver 가 .sr-grid 에 .in 을 붙이면 카드가 순서대로 떠오른다
.sr-card {
opacity: 0;
transform: translateY($sp-4);
transition: opacity 400ms $easing, transform 400ms $easing;
}
.sr-grid.in .sr-card { opacity: 1; transform: none; }
.sr-grid.in .sr-card:nth-child(2) { transition-delay: 140ms; }
.sr-grid.in .sr-card:nth-child(3) { transition-delay: 280ms; }
// 데모 루프: is-demo 는 실사용 경로(.in, 실제 스크롤 진입)와 별개로 keyframes 로 반복 재생 (캡처용)
@keyframes sr-rise {
0% { opacity: 0; transform: translateY($sp-4); }
20%, 85% { opacity: 1; transform: none; }
100% { opacity: 0; transform: translateY($sp-4); }
}
.sr-grid.is-demo .sr-card { animation: sr-rise $duration $easing infinite; }
.sr-grid.is-demo .sr-card:nth-child(2) { animation-delay: 160ms; }
.sr-grid.is-demo .sr-card:nth-child(3) { animation-delay: 320ms; }
@media (prefers-reduced-motion: reduce) {
.sr-card,
.sr-grid.is-demo .sr-card {
animation: none !important;
transition: none !important;
opacity: 1 !important;
transform: none !important;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sr-grid {
width: min(340px, 90vw);
aspect-ratio: 340/190;
display: flex;
align-items: flex-end;
gap: 8px;
}
.sr-card {
flex: 1 1 0;
height: 78%;
border-radius: 8px;
box-sizing: border-box;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 4px;
color: #fff7e6;
background: rgba(255, 255, 255, 0.08);
font: 600 12px/1 -apple-system, "Segoe UI", sans-serif;
}
.sr-card--mid {
height: 100%;
background: #0a4fe8;
}
.sr-card__tag {
opacity: 1;
font-size: 11px;
}
.sr-card__price {
font-size: 20px;
font-weight: 700;
margin-top: 4px;
}
.sr-card {
opacity: 0;
transform: translateY(16px);
transition: opacity 400ms ease-out, transform 400ms ease-out;
}
.sr-grid.in .sr-card {
opacity: 1;
transform: none;
}
.sr-grid.in .sr-card:nth-child(2) {
transition-delay: 140ms;
}
.sr-grid.in .sr-card:nth-child(3) {
transition-delay: 280ms;
}
@keyframes sr-rise {
0% {
opacity: 0;
transform: translateY(16px);
}
20%, 85% {
opacity: 1;
transform: none;
}
100% {
opacity: 0;
transform: translateY(16px);
}
}
.sr-grid.is-demo .sr-card {
animation: sr-rise 2s ease-out infinite;
}
.sr-grid.is-demo .sr-card:nth-child(2) {
animation-delay: 160ms;
}
.sr-grid.is-demo .sr-card:nth-child(3) {
animation-delay: 320ms;
}
@media (prefers-reduced-motion: reduce) {
.sr-card,
.sr-grid.is-demo .sr-card {
animation: none !important;
transition: none !important;
opacity: 1 !important;
transform: none !important;
}
}var grid = document.querySelector('.sr-grid');
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { grid.classList.add('in'); io.unobserve(grid); }
});
}, { threshold: 0.3 });
io.observe(grid);import { CSSProperties, useEffect, useRef, useState } from "react";
import styles from "./PlansRevealOnScroll.module.scss";
export interface PlansRevealOnScrollProps {
duration?: string;
easing?: string;
stagger?: string;
}
const PLANS = [
{ tag: "Basic", price: "$9" },
{ tag: "Pro", price: "$29" },
{ tag: "Team", price: "$79" },
];
// 실사용: 진짜 IntersectionObserver 가 그리드가 뷰포트에 들어올 때 in 을 켠다. 카드마다 transition-delay 만 다르다.
export default function PlansRevealOnScroll({
duration = "400ms",
easing = "ease-out",
stagger = "140ms",
}: PlansRevealOnScrollProps) {
const gridRef = useRef<HTMLDivElement>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = gridRef.current;
if (!el) return;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => { if (e.isIntersecting) { setInView(true); io.unobserve(e.target); } });
}, { threshold: 0.3 });
io.observe(el);
return () => io.disconnect();
}, []);
const style = { "--duration": duration, "--easing": easing } as CSSProperties;
return (
<div className={styles.grid} style={style} ref={gridRef}>
{PLANS.map((p, i) => (
<div
key={p.tag}
className={`${styles.card} ${i === 1 ? styles.mid : ""} ${inView ? styles.in : ""}`}
style={{ transitionDelay: inView ? `calc(${stagger} * ${i})` : undefined }}
>
<span className={styles.tag}>{p.tag}</span>
<span className={styles.price}>{p.price}</span>
</div>
))}
</div>
);
}01 스크롤 리빌 플랜 — 측정 누적면적 37.606% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 가격표 디자인 애니메이션 9종
이 데모가 속한 글
CSS 가격표 디자인 애니메이션 9종가격표 디자인에 마우스를 올렸을 때 카드가 살짝 떠오르거나 가격 숫자가 휙 바뀌는 걸 본 적 있으신가요? 어떤 요금제를 고를지 망설이는 그 짧은 순간, 이런 작은 움직임 하나가 시선을 붙잡아 줍니다. 이 글은 그런 이 글의 데모 전부 보기


