04가격 앵커 비교 스택
정가에 취소선이 그어지고 할인가·쿠폰가가 한 줄씩 내려앉아 마지막에 절약 금액이 한 번 튀는데, 네 줄이 한 패널에 쌓여 있어 원래 가격이 기준점으로 남습니다.
쓰임첫 달 할인 안내, 프로모션 결제 직전 가격 정리
animation-delayscaleXaria-expanded

<main class="stage is-demo">
<section class="ap">
<header class="ap__head">
<span class="ap__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="20" height="20"><g fill="currentColor"><path d="M237.66 153L153 237.66a8 8 0 0 1-11.31 0l-99.35-99.32a8 8 0 0 1-2.34-5.65V40h92.69a8 8 0 0 1 5.65 2.34l99.32 99.32a8 8 0 0 1 0 11.34" opacity=".2"/><path d="M243.31 136L144 36.69A15.86 15.86 0 0 0 132.69 32H40a8 8 0 0 0-8 8v92.69A15.86 15.86 0 0 0 36.69 144L136 243.31a16 16 0 0 0 22.63 0l84.68-84.68a16 16 0 0 0 0-22.63m-96 96L48 132.69V48h84.69L232 147.31ZM96 84a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg></span>
<div class="ap__tt">
<p class="ap__plan">스타터 플랜</p>
<p class="ap__sub">첫 달 요금 알아보기</p>
</div>
</header>
<button type="button" class="ap__toggle" aria-expanded="false" aria-controls="ap-panel">
<span class="ap__toggle-t">가격 구성 보기</span>
<svg class="ap__chev" viewBox="0 0 24 24" width="12" height="12" aria-hidden="true"><polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ap__panel" id="ap-panel">
<div class="ap__panel-in">
<ul class="ap__stack">
<li class="ap__row" style="--i:0">
<span class="ap__label">정가</span>
<span class="ap__orig">₩59,000<span class="ap__strike" aria-hidden="true"></span></span>
</li>
<li class="ap__row" style="--i:1">
<span class="ap__label">첫 달 할인가</span>
<b class="ap__deal">₩29,000</b>
</li>
<li class="ap__row" style="--i:2">
<span class="ap__label">쿠폰 적용가</span>
<span class="ap__final">₩20,300</span>
</li>
<li class="ap__row ap__row--save" style="--i:3">
<span class="ap__save">₩38,700 절약</span>
</li>
</ul>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-paper);
.ap {
width: min(300px, 94vw);
box-sizing: border-box;
background: $stage-paper;
border: 1px solid rgba(23, 20, 26, .1);
border-radius: $r-card;
box-shadow: $shadow-press;
padding: $sp-4;
}
.ap__head { display: flex; align-items: center; gap: $sp-2; }
.ap__ic { display: grid; place-items: center; flex: 0 0 auto; color: $color; }
.ap__tt { display: flex; flex-direction: column; font-family: inherit; }
.ap__plan { margin: 0; font-weight: 800; font-size: 13px; line-height: 1.2; color: $subject-ink; }
.ap__sub { margin: 0; font-weight: 500; font-size: 11px; line-height: 1.4; color: rgba(23, 20, 26, .72); }
// ── 토글 버튼 ────────────────────────────────────────
.ap__toggle {
display: flex; align-items: center; justify-content: center; gap: $sp-1;
width: 100%; margin-top: $sp-3;
padding: $sp-2 $sp-3;
border: 1.5px solid rgba(47, 109, 246, .28);
border-radius: $r-control;
background: rgba(47, 109, 246, .08);
color: $subject-blue-deep; // 연파랑 면 위 파란 글자 4.10:1 → deep 5.78:1
font-family: inherit; font-weight: 800; font-size: 12px; line-height: 1;
cursor: pointer;
}
.ap__toggle:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
.ap__chev { transition: transform $dur-base $easing; }
.ap.is-open .ap__chev { transform: rotate(180deg); }
// ── 펼침 패널 (grid 0fr → 1fr) ───────────────────────
.ap__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows $dur-base $easing; }
.ap.is-open .ap__panel { grid-template-rows: 1fr; }
.ap__panel-in { overflow: hidden; }
.ap__stack { list-style: none; margin: 0; padding: $sp-3 0 0; display: flex; flex-direction: column; gap: $sp-2; }
.ap__row {
display: flex; align-items: baseline; justify-content: space-between; gap: $sp-2;
font-family: inherit;
}
.ap__label { font-weight: 600; font-size: 11px; line-height: 1.2; color: rgba(23, 20, 26, .72); }
.ap__orig { position: relative; font-weight: 600; font-size: 13px; line-height: 1.2; color: rgba(23, 20, 26, .72); }
.ap__strike {
position: absolute; left: 0; right: 0; top: 50%; height: 2px;
background: $error; border-radius: 0;
transform: scaleX(0); transform-origin: left center; // 규칙8: 좌변서 그어짐
transition: transform $dur-base $easing;
}
.ap.is-open .ap__strike { transform: scaleX(1); }
.ap__deal { font-weight: 800; font-size: 22px; line-height: 1.1; color: $color; font-variant-numeric: tabular-nums; }
.ap__final { font-weight: 800; font-size: 15px; line-height: 1.2; color: $subject-ink; font-variant-numeric: tabular-nums; }
.ap__row--save { justify-content: flex-end; }
.ap__save {
display: inline-block;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: $error; color: $subject-cream;
font-weight: 800; font-size: 11px; line-height: 1.3;
}
// ── 측정용 루프 (.is-demo) ───────────────────────────
// 패널을 펼친 채로 네 줄이 시차 등장하고, 90% 지점에서 컷으로 되돌아 다음 바퀴를 돈다.
.stage.is-demo .ap__panel { grid-template-rows: 1fr; }
.stage.is-demo .ap__chev { transform: rotate(180deg); }
.stage.is-demo .ap__row {
animation-name: ap-rise;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 80ms);
}
// 마지막 줄(--i:3, 지연 240ms)까지 2s 루프의 90% 안에 되돌아오도록 78% 에서 컷
@keyframes ap-rise {
0% { opacity: 0; transform: translateY(10px); }
12% { opacity: 1; transform: translateY(0); }
74% { opacity: 1; transform: translateY(0); }
78%, 100% { opacity: 0; transform: translateY(10px); }
}
.stage.is-demo .ap__strike {
transition: none;
animation-name: ap-strike;
animation-duration: $duration;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: 120ms;
}
@keyframes ap-strike {
0% { transform: scaleX(0); }
24%, 74% { transform: scaleX(1); }
78%, 100% { transform: scaleX(0); }
}
.stage.is-demo .ap__save {
animation-name: ap-pop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: 240ms;
}
@keyframes ap-pop {
0% { transform: scale(.6); }
18% { transform: scale(1.14); }
28%, 74% { transform: scale(1); }
78%, 100% { transform: scale(.6); }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.ap__panel, .ap__chev, .ap__strike { transition: none !important; }
.stage.is-demo .ap__row,
.stage.is-demo .ap__strike,
.stage.is-demo .ap__save { animation: none !important; }
.stage.is-demo .ap__row { opacity: 1 !important; transform: none !important; }
.stage.is-demo .ap__strike { transform: scaleX(1) !important; }
.stage.is-demo .ap__save { transform: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.ap { width: min(268px, 92vw); padding: $sp-2; }
.ap__ic svg { width: 16px; height: 16px; }
.ap__plan { font-size: 11px; }
.ap__sub { font-size: 9px; }
.ap__toggle { margin-top: $sp-2; padding: $sp-1 $sp-2; font-size: 10px; }
.ap__stack { padding: $sp-2 0 0; gap: $sp-1; }
.ap__label { font-size: 9px; }
.ap__orig { font-size: 11px; }
.ap__deal { font-size: 17px; }
.ap__final { font-size: 12px; }
.ap__save { padding: 0 $sp-2; font-size: 9px; }
}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);
}
.ap {
width: min(300px, 94vw);
box-sizing: border-box;
background: #ffffff;
border: 1px solid rgba(23, 20, 26, 0.1);
border-radius: 16px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
padding: 16px;
}
.ap__head {
display: flex;
align-items: center;
gap: 8px;
}
.ap__ic {
display: grid;
place-items: center;
flex: 0 0 auto;
color: #2f6df6;
}
.ap__tt {
display: flex;
flex-direction: column;
font-family: inherit;
}
.ap__plan {
margin: 0;
font-weight: 800;
font-size: 13px;
line-height: 1.2;
color: #17141a;
}
.ap__sub {
margin: 0;
font-weight: 500;
font-size: 11px;
line-height: 1.4;
color: rgba(23, 20, 26, 0.72);
}
.ap__toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
margin-top: 12px;
padding: 8px 12px;
border: 1.5px solid rgba(47, 109, 246, 0.28);
border-radius: 12px;
background: rgba(47, 109, 246, 0.08);
color: #0a4fe8;
font-family: inherit;
font-weight: 800;
font-size: 12px;
line-height: 1;
cursor: pointer;
}
.ap__toggle:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ap__chev {
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ap.is-open .ap__chev {
transform: rotate(180deg);
}
.ap__panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ap.is-open .ap__panel {
grid-template-rows: 1fr;
}
.ap__panel-in {
overflow: hidden;
}
.ap__stack {
list-style: none;
margin: 0;
padding: 12px 0 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.ap__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
font-family: inherit;
}
.ap__label {
font-weight: 600;
font-size: 11px;
line-height: 1.2;
color: rgba(23, 20, 26, 0.72);
}
.ap__orig {
position: relative;
font-weight: 600;
font-size: 13px;
line-height: 1.2;
color: rgba(23, 20, 26, 0.72);
}
.ap__strike {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
background: #c2361a;
border-radius: 0;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ap.is-open .ap__strike {
transform: scaleX(1);
}
.ap__deal {
font-weight: 800;
font-size: 22px;
line-height: 1.1;
color: #2f6df6;
font-variant-numeric: tabular-nums;
}
.ap__final {
font-weight: 800;
font-size: 15px;
line-height: 1.2;
color: #17141a;
font-variant-numeric: tabular-nums;
}
.ap__row--save {
justify-content: flex-end;
}
.ap__save {
display: inline-block;
padding: 4px 12px;
border-radius: 999px;
background: #c2361a;
color: #fff7e6;
font-weight: 800;
font-size: 11px;
line-height: 1.3;
}
.stage.is-demo .ap__panel {
grid-template-rows: 1fr;
}
.stage.is-demo .ap__chev {
transform: rotate(180deg);
}
.stage.is-demo .ap__row {
animation-name: ap-rise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 80ms);
}
@keyframes ap-rise {
0% {
opacity: 0;
transform: translateY(10px);
}
12% {
opacity: 1;
transform: translateY(0);
}
74% {
opacity: 1;
transform: translateY(0);
}
78%, 100% {
opacity: 0;
transform: translateY(10px);
}
}
.stage.is-demo .ap__strike {
transition: none;
animation-name: ap-strike;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: 120ms;
}
@keyframes ap-strike {
0% {
transform: scaleX(0);
}
24%, 74% {
transform: scaleX(1);
}
78%, 100% {
transform: scaleX(0);
}
}
.stage.is-demo .ap__save {
animation-name: ap-pop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: 240ms;
}
@keyframes ap-pop {
0% {
transform: scale(0.6);
}
18% {
transform: scale(1.14);
}
28%, 74% {
transform: scale(1);
}
78%, 100% {
transform: scale(0.6);
}
}
@media (prefers-reduced-motion: reduce) {
.ap__panel, .ap__chev, .ap__strike {
transition: none !important;
}
.stage.is-demo .ap__row,
.stage.is-demo .ap__strike,
.stage.is-demo .ap__save {
animation: none !important;
}
.stage.is-demo .ap__row {
opacity: 1 !important;
transform: none !important;
}
.stage.is-demo .ap__strike {
transform: scaleX(1) !important;
}
.stage.is-demo .ap__save {
transform: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.ap {
width: min(268px, 92vw);
padding: 8px;
}
.ap__ic svg {
width: 16px;
height: 16px;
}
.ap__plan {
font-size: 11px;
}
.ap__sub {
font-size: 9px;
}
.ap__toggle {
margin-top: 8px;
padding: 4px 8px;
font-size: 10px;
}
.ap__stack {
padding: 8px 0 0;
gap: 4px;
}
.ap__label {
font-size: 9px;
}
.ap__orig {
font-size: 11px;
}
.ap__deal {
font-size: 17px;
}
.ap__final {
font-size: 12px;
}
.ap__save {
padding: 0 8px;
font-size: 9px;
}
}// 실제 동작: 토글 버튼을 누르면 패널이 grid 0fr→1fr 로 펼쳐지고 네 줄이 시차 등장한다.
// 처음 조작(pointerdown·keydown 1회)에서 측정용 루프 is-demo 를 뗀다.
var stage = document.querySelector('.stage');
var ap = document.querySelector('.ap');
var btn = document.querySelector('.ap__toggle');
['pointerdown', 'keydown'].forEach(function (t) {
window.addEventListener(t, function () { stage.classList.remove('is-demo'); }, { once: true });
});
btn.addEventListener('click', function () {
var open = ap.classList.toggle('is-open');
btn.setAttribute('aria-expanded', String(open));
});import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./AnchorPriceStack.module.scss";
export type AnchorPriceStackProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string; "--i"?: string };
const ROWS = [
{ label: "정가", kind: "orig" },
{ label: "첫 달 할인가", kind: "deal" },
{ label: "쿠폰 적용가", kind: "final" },
{ label: "", kind: "save" },
];
export default function AnchorPriceStack({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: AnchorPriceStackProps) {
const [open, setOpen] = useState(false);
// 측정용 루프: 처음 조작(pointerdown·keydown)에서 루프를 끈다 — vanilla 판과 같은 동작
const [demo, setDemo] = useState(true);
useEffect(() => {
const off = () => setDemo(false);
window.addEventListener("pointerdown", off, { once: true });
window.addEventListener("keydown", off, { once: true });
return () => {
window.removeEventListener("pointerdown", off);
window.removeEventListener("keydown", off);
};
}, []);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const cls = [styles.ap, open ? styles.isOpen : "", demo ? styles.isDemo : ""].join(" ");
return (
<section className={cls} style={vars}>
<header className={styles.head}>
<span className={styles.ic} aria-hidden="true">
<svg viewBox="0 0 256 256" width="20" height="20">
<g fill="currentColor">
<path d="M237.66 153L153 237.66a8 8 0 0 1-11.31 0l-99.35-99.32a8 8 0 0 1-2.34-5.65V40h92.69a8 8 0 0 1 5.65 2.34l99.32 99.32a8 8 0 0 1 0 11.34" opacity=".2" />
<path d="M243.31 136L144 36.69A15.86 15.86 0 0 0 132.69 32H40a8 8 0 0 0-8 8v92.69A15.86 15.86 0 0 0 36.69 144L136 243.31a16 16 0 0 0 22.63 0l84.68-84.68a16 16 0 0 0 0-22.63m-96 96L48 132.69V48h84.69L232 147.31ZM96 84a12 12 0 1 1-12-12a12 12 0 0 1 12 12" />
</g>
</svg>
</span>
<div className={styles.tt}>
<p className={styles.plan}>스타터 플랜</p>
<p className={styles.sub}>첫 달 요금 알아보기</p>
</div>
</header>
<button
type="button"
className={styles.toggle}
aria-expanded={open}
aria-controls="ap-panel"
onClick={() => setOpen(!open)}
>
<span>가격 구성 보기</span>
<svg className={styles.chev} viewBox="0 0 24 24" width="12" height="12" aria-hidden="true">
<polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div className={styles.panel} id="ap-panel">
<div className={styles.panelIn}>
<ul className={styles.stack}>
{ROWS.map((r, i) => {
const rowVars: Vars = { "--i": String(i) };
if (r.kind === "save") {
return (
<li key={r.kind} className={`${styles.row} ${styles.rowSave}`} style={rowVars}>
<span className={styles.save}>₩38,700 절약</span>
</li>
);
}
return (
<li key={r.kind} className={styles.row} style={rowVars}>
<span className={styles.label}>{r.label}</span>
{r.kind === "orig" && (
<span className={styles.orig}>
₩59,000
<span className={styles.strike} aria-hidden="true" />
</span>
)}
{r.kind === "deal" && <b className={styles.deal}>₩29,000</b>}
{r.kind === "final" && <span className={styles.final}>₩20,300</span>}
</li>
);
})}
</ul>
</div>
</div>
</section>
);
}04 가격 앵커 비교 스택 — 측정 누적면적 4.901% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 랜딩페이지 전환율을 끌어올리는 섹션 9종
이 데모가 속한 글
랜딩페이지 전환율을 끌어올리는 섹션 9종랜딩페이지 전환율이란 방문자가 이메일을 남기거나 예약·구매까지 가는 비율을 말하고, 이 글은 그 전환을 직접 받아내는 섹션 아홉 개를 통째로 주는 코드 모음입니다. 떠나는 방문자를 붙잡는 이탈 팝업부터 읽던 지점에 이 글의 데모 전부 보기


