01Plans reveal on scroll
As the pricing section scrolls into view, each plan card rises into place one after another so visitors read them in order.
Best forPricing landing pages, scroll-revealed plan comparison sections
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 Plans reveal on scroll — Measured area 37.606% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS Pricing Table Animations You Can Copy-Paste
The post this demo lives in
9 CSS Pricing Table Animations You Can Copy-PasteA pricing table is the Free/Pro/Team grid every subscription site shows before checkout, and a good CSS pricing table ad See all demos in this post


