05이의 해소 FAQ 와 도달 CTA
구매를 망설이는 이유 세 개를 하나씩 열어 답을 보면 그 줄에 해소 표시가 붙고, 세 개가 다 풀리는 순간 아래 버튼이 커지며 누를 준비가 됐다는 문장이 같이 바뀝니다.
쓰임요금제·구독 안내의 마지막 망설임 구간, 결제 직전 FAQ
grid-template-rowsaria-expandedscale

<main class="stage is-demo">
<section class="ob">
<p class="ob__title">
<span class="ob__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="18" height="18"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/></g></svg></span>
아직 망설이는 부분
</p>
<ul class="ob__list">
<li class="ob__item" style="--i:0">
<button type="button" class="ob__q" aria-expanded="false" aria-controls="ob-a1">
<span class="ob__q-t">가격이 부담돼요</span>
<span class="ob__done">
<svg class="ob__check" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity=".35"/><polyline points="7 12.5 10.5 16 17 8.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="ob__done-t">해소됨</span>
</span>
</button>
<div class="ob__a" id="ob-a1"><div class="ob__a-in"><p>첫 달 ₩0, 이후엔 쓴 만큼만 냅니다.</p></div></div>
</li>
<li class="ob__item" style="--i:1">
<button type="button" class="ob__q" aria-expanded="false" aria-controls="ob-a2">
<span class="ob__q-t">써보지 않고 믿기 어려워요</span>
<span class="ob__done">
<svg class="ob__check" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity=".35"/><polyline points="7 12.5 10.5 16 17 8.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="ob__done-t">해소됨</span>
</span>
</button>
<div class="ob__a" id="ob-a2"><div class="ob__a-in"><p>카드 없이 14일 동안 전체 기능을 씁니다.</p></div></div>
</li>
<li class="ob__item" style="--i:2">
<button type="button" class="ob__q" aria-expanded="false" aria-controls="ob-a3">
<span class="ob__q-t">해지가 번거로울까 봐요</span>
<span class="ob__done">
<svg class="ob__check" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity=".35"/><polyline points="7 12.5 10.5 16 17 8.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="ob__done-t">해소됨</span>
</span>
</button>
<div class="ob__a" id="ob-a3"><div class="ob__a-in"><p>설정에서 버튼 한 번이면 끝납니다.</p></div></div>
</li>
</ul>
<button type="button" class="ob__cta">
<span class="ob__cta-t">자유롭게 시작하기</span>
<span class="ob__cta-t ob__cta-t--ready" aria-hidden="true">준비됐어요, 눌러주세요</span>
</button>
<p class="ob__status" role="status"></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-ink);
.ob {
width: min(312px, 94vw);
box-sizing: border-box;
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
padding: $sp-3;
}
.ob__title {
display: flex; align-items: center; gap: $sp-1;
margin: 0 0 $sp-2;
font-family: inherit; font-weight: 800; font-size: 13px; line-height: 1.2;
color: $subject-ink;
}
.ob__ic { display: grid; place-items: center; color: $color; }
.ob__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
.ob__item { border-bottom: 1px solid rgba(23, 20, 26, .1); }
.ob__q {
display: flex; align-items: center; justify-content: space-between; gap: $sp-2;
width: 100%; padding: $sp-1 0;
border: 0; background: transparent; cursor: pointer; text-align: left;
font-family: inherit; font-weight: 700; font-size: 12px; line-height: 1.2;
color: $subject-ink;
}
.ob__q:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.ob__done {
display: inline-flex; align-items: center; gap: $sp-1; flex: 0 0 auto;
color: #50a26e; font-weight: 800; font-size: 10px; line-height: 1;
opacity: 0; transform: scale(.8);
transition: opacity $dur-quick $ease-out, transform $dur-quick $easing;
}
.ob__item.is-open .ob__done { opacity: 1; transform: scale(1); }
.ob__check polyline { stroke-dasharray: 26; stroke-dashoffset: 26; transition: stroke-dashoffset $dur-base $ease-out; }
.ob__item.is-open .ob__check polyline { stroke-dashoffset: 0; }
// ── 펼침 (grid 0fr → 1fr) ────────────────────────────
.ob__a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows $dur-base $easing; }
.ob__item.is-open .ob__a { grid-template-rows: 1fr; }
.ob__a-in { overflow: hidden; }
.ob__a p {
margin: 0; padding: 0 0 $sp-1;
font-family: inherit; font-weight: 500; font-size: 11px; line-height: 1.5;
color: rgba(23, 20, 26, .66);
}
// ── CTA ──────────────────────────────────────────────
.ob__cta {
display: grid; place-items: center;
width: 100%; margin-top: $sp-2;
padding: $sp-2 $sp-3;
border: 0; border-radius: $r-control;
background: $color; color: $subject-cream;
font-family: inherit; font-weight: 800; font-size: 12px; line-height: 1.2;
cursor: pointer;
}
.ob__cta:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
.ob__cta-t { grid-area: 1 / 1; }
.ob__cta-t--ready { opacity: 0; }
.ob.is-ready .ob__cta-t { opacity: 0; }
.ob.is-ready .ob__cta-t--ready { opacity: 1; }
.ob.is-ready .ob__cta { animation: ob-pulse $duration $easing infinite; }
@keyframes ob-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.ob__status { margin: $sp-1 0 0; min-height: 13px; font-family: inherit; font-weight: 600; font-size: 10px; line-height: 1.3; color: rgba(23, 20, 26, .55); }
// ── 측정용 루프 (.is-demo) ───────────────────────────
// 세 줄이 차례로 펼쳐지고(지연 160ms 간격) CTA 글자가 컷으로 바뀐 뒤 펄스한다. 2s 루프의 90% 안에 복귀.
.stage.is-demo .ob__a {
transition: none;
animation-name: ob-open;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 160ms);
}
// 닫힘은 열림보다 훨씬 느리게 편다 — 프레임당 변한 면적이 작아져 대표 이미지(poster)가 닫히는 장면으로 뽑히지 않는다
@keyframes ob-open {
0% { grid-template-rows: 0fr; }
16% { grid-template-rows: 1fr; }
66% { grid-template-rows: 1fr; }
100% { grid-template-rows: 0fr; }
}
.stage.is-demo .ob__a p {
animation-name: ob-answer;
animation-duration: $duration;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 160ms);
}
@keyframes ob-answer {
0% { opacity: 0; transform: translateY(6px); }
20%, 66% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(6px); }
}
.stage.is-demo .ob__done {
transition: none;
animation-name: ob-done;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 160ms);
}
@keyframes ob-done {
0% { opacity: 0; transform: scale(.8); }
14% { opacity: 1; transform: scale(1.16); }
24%, 66% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(.8); }
}
.stage.is-demo .ob__check polyline {
transition: none;
animation-name: ob-draw;
animation-duration: $duration;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 160ms);
}
@keyframes ob-draw {
0% { stroke-dashoffset: 26; }
28%, 66% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: 26; }
}
// 글자 교체는 컷(steps), 커지는 느낌은 버튼 scale 로 따로 준다
.stage.is-demo .ob__cta-t {
animation-name: ob-label-out;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.stage.is-demo .ob__cta-t--ready {
animation-name: ob-label-in;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
// 라벨이 「준비됐어요」인 구간은 세 줄이 전부 열려 있는 동안(41~64%)만 — 한 줄이라도 닫히기 시작하면 원래 라벨로 돌아온다
@keyframes ob-label-out {
0%, 40% { opacity: 1; }
41%, 64% { opacity: 0; }
65%, 100% { opacity: 1; }
}
@keyframes ob-label-in {
0%, 40% { opacity: 0; }
41%, 64% { opacity: 1; }
65%, 100% { opacity: 0; }
}
.stage.is-demo .ob__cta {
animation-name: ob-cta-loop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes ob-cta-loop {
0%, 41% { transform: scale(1); }
48% { transform: scale(1.06); }
55% { transform: scale(1); }
60% { transform: scale(1.04); }
64%, 100% { transform: scale(1); }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.ob__a, .ob__done, .ob__check polyline { transition: none !important; }
.stage.is-demo .ob__a,
.stage.is-demo .ob__a p,
.stage.is-demo .ob__done,
.stage.is-demo .ob__check polyline,
.stage.is-demo .ob__cta,
.stage.is-demo .ob__cta-t,
.stage.is-demo .ob__cta-t--ready,
.ob.is-ready .ob__cta { animation: none !important; }
.stage.is-demo .ob__a { grid-template-rows: 1fr !important; }
.stage.is-demo .ob__a p { opacity: 1 !important; transform: none !important; }
.stage.is-demo .ob__done { opacity: 1 !important; transform: none !important; }
.stage.is-demo .ob__check polyline { stroke-dashoffset: 0 !important; }
.stage.is-demo .ob__cta { transform: none !important; }
.stage.is-demo .ob__cta-t { opacity: 0 !important; }
.stage.is-demo .ob__cta-t--ready { opacity: 1 !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.ob { width: min(272px, 92vw); padding: $sp-2; }
.ob__title { margin: 0 0 $sp-1; font-size: 11px; }
.ob__ic svg { width: 14px; height: 14px; }
.ob__list { gap: 0; }
.ob__q { padding: $sp-1 0; font-size: 10px; }
.ob__done { font-size: 9px; }
.ob__check { width: 12px; height: 12px; }
// 세 줄이 동시에 열린 순간까지 200px 안에 들어가도록 답 줄을 8px·여백 0 으로 내린다 (실측 2026-09-12)
.ob__a p {
padding: 0; font-size: 8px; line-height: 1.2;
display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
.ob__cta { margin-top: $sp-2; padding: $sp-1 $sp-2; font-size: 10px; }
.ob__status { margin-top: 0; min-height: 0; 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ob {
width: min(312px, 94vw);
box-sizing: border-box;
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 12px;
}
.ob__title {
display: flex;
align-items: center;
gap: 4px;
margin: 0 0 8px;
font-family: inherit;
font-weight: 800;
font-size: 13px;
line-height: 1.2;
color: #17141a;
}
.ob__ic {
display: grid;
place-items: center;
color: #2f6df6;
}
.ob__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.ob__item {
border-bottom: 1px solid rgba(23, 20, 26, 0.1);
}
.ob__q {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 4px 0;
border: 0;
background: transparent;
cursor: pointer;
text-align: left;
font-family: inherit;
font-weight: 700;
font-size: 12px;
line-height: 1.2;
color: #17141a;
}
.ob__q:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ob__done {
display: inline-flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
color: #50a26e;
font-weight: 800;
font-size: 10px;
line-height: 1;
opacity: 0;
transform: scale(0.8);
transition: opacity 220ms ease-out, transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ob__item.is-open .ob__done {
opacity: 1;
transform: scale(1);
}
.ob__check polyline {
stroke-dasharray: 26;
stroke-dashoffset: 26;
transition: stroke-dashoffset 300ms ease-out;
}
.ob__item.is-open .ob__check polyline {
stroke-dashoffset: 0;
}
.ob__a {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ob__item.is-open .ob__a {
grid-template-rows: 1fr;
}
.ob__a-in {
overflow: hidden;
}
.ob__a p {
margin: 0;
padding: 0 0 4px;
font-family: inherit;
font-weight: 500;
font-size: 11px;
line-height: 1.5;
color: rgba(23, 20, 26, 0.66);
}
.ob__cta {
display: grid;
place-items: center;
width: 100%;
margin-top: 8px;
padding: 8px 12px;
border: 0;
border-radius: 12px;
background: #2f6df6;
color: #fff7e6;
font-family: inherit;
font-weight: 800;
font-size: 12px;
line-height: 1.2;
cursor: pointer;
}
.ob__cta:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.ob__cta-t {
grid-area: 1/1;
}
.ob__cta-t--ready {
opacity: 0;
}
.ob.is-ready .ob__cta-t {
opacity: 0;
}
.ob.is-ready .ob__cta-t--ready {
opacity: 1;
}
.ob.is-ready .ob__cta {
animation: ob-pulse 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes ob-pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
.ob__status {
margin: 4px 0 0;
min-height: 13px;
font-family: inherit;
font-weight: 600;
font-size: 10px;
line-height: 1.3;
color: rgba(23, 20, 26, 0.55);
}
.stage.is-demo .ob__a {
transition: none;
animation-name: ob-open;
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) * 160ms);
}
@keyframes ob-open {
0% {
grid-template-rows: 0fr;
}
16% {
grid-template-rows: 1fr;
}
66% {
grid-template-rows: 1fr;
}
100% {
grid-template-rows: 0fr;
}
}
.stage.is-demo .ob__a p {
animation-name: ob-answer;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 160ms);
}
@keyframes ob-answer {
0% {
opacity: 0;
transform: translateY(6px);
}
20%, 66% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(6px);
}
}
.stage.is-demo .ob__done {
transition: none;
animation-name: ob-done;
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) * 160ms);
}
@keyframes ob-done {
0% {
opacity: 0;
transform: scale(0.8);
}
14% {
opacity: 1;
transform: scale(1.16);
}
24%, 66% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.8);
}
}
.stage.is-demo .ob__check polyline {
transition: none;
animation-name: ob-draw;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
animation-delay: calc(var(--i) * 160ms);
}
@keyframes ob-draw {
0% {
stroke-dashoffset: 26;
}
28%, 66% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 26;
}
}
.stage.is-demo .ob__cta-t {
animation-name: ob-label-out;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.stage.is-demo .ob__cta-t--ready {
animation-name: ob-label-in;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ob-label-out {
0%, 40% {
opacity: 1;
}
41%, 64% {
opacity: 0;
}
65%, 100% {
opacity: 1;
}
}
@keyframes ob-label-in {
0%, 40% {
opacity: 0;
}
41%, 64% {
opacity: 1;
}
65%, 100% {
opacity: 0;
}
}
.stage.is-demo .ob__cta {
animation-name: ob-cta-loop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
@keyframes ob-cta-loop {
0%, 41% {
transform: scale(1);
}
48% {
transform: scale(1.06);
}
55% {
transform: scale(1);
}
60% {
transform: scale(1.04);
}
64%, 100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.ob__a, .ob__done, .ob__check polyline {
transition: none !important;
}
.stage.is-demo .ob__a,
.stage.is-demo .ob__a p,
.stage.is-demo .ob__done,
.stage.is-demo .ob__check polyline,
.stage.is-demo .ob__cta,
.stage.is-demo .ob__cta-t,
.stage.is-demo .ob__cta-t--ready,
.ob.is-ready .ob__cta {
animation: none !important;
}
.stage.is-demo .ob__a {
grid-template-rows: 1fr !important;
}
.stage.is-demo .ob__a p {
opacity: 1 !important;
transform: none !important;
}
.stage.is-demo .ob__done {
opacity: 1 !important;
transform: none !important;
}
.stage.is-demo .ob__check polyline {
stroke-dashoffset: 0 !important;
}
.stage.is-demo .ob__cta {
transform: none !important;
}
.stage.is-demo .ob__cta-t {
opacity: 0 !important;
}
.stage.is-demo .ob__cta-t--ready {
opacity: 1 !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.ob {
width: min(272px, 92vw);
padding: 8px;
}
.ob__title {
margin: 0 0 4px;
font-size: 11px;
}
.ob__ic svg {
width: 14px;
height: 14px;
}
.ob__list {
gap: 0;
}
.ob__q {
padding: 4px 0;
font-size: 10px;
}
.ob__done {
font-size: 9px;
}
.ob__check {
width: 12px;
height: 12px;
}
.ob__a p {
padding: 0;
font-size: 8px;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ob__cta {
margin-top: 8px;
padding: 4px 8px;
font-size: 10px;
}
.ob__status {
margin-top: 0;
min-height: 0;
font-size: 9px;
}
}// 실제 동작: 질문 줄을 누르면 답이 grid 0fr→1fr 로 펼쳐지고 그 줄에 체크가 그려진다.
// 세 줄을 모두 한 번씩 열면 CTA 글자가 바뀌고 펄스가 시작된다.
var stage = document.querySelector('.stage');
var ob = document.querySelector('.ob');
var items = [].slice.call(document.querySelectorAll('.ob__item'));
var status = document.querySelector('.ob__status');
var label = document.querySelector('.ob__cta-t');
var labelReady = document.querySelector('.ob__cta-t--ready');
var opened = [];
['pointerdown', 'keydown'].forEach(function (t) {
window.addEventListener(t, function () { stage.classList.remove('is-demo'); }, { once: true });
});
items.forEach(function (item, idx) {
var q = item.querySelector('.ob__q');
q.addEventListener('click', function () {
var open = item.classList.toggle('is-open');
q.setAttribute('aria-expanded', String(open));
if (open && opened.indexOf(idx) < 0) opened.push(idx);
if (opened.length === items.length && !ob.classList.contains('is-ready')) {
ob.classList.add('is-ready');
label.setAttribute('aria-hidden', 'true');
labelReady.removeAttribute('aria-hidden');
status.textContent = '모든 걸림돌이 풀렸어요';
}
});
});import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ObjectionFaqCta.module.scss";
export type ObjectionFaqCtaProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string; "--i"?: string };
const ITEMS = [
{ q: "가격이 부담돼요", a: "첫 달 ₩0, 이후엔 쓴 만큼만 냅니다." },
{ q: "써보지 않고 믿기 어려워요", a: "카드 없이 14일 동안 전체 기능을 씁니다." },
{ q: "해지가 번거로울까 봐요", a: "설정에서 버튼 한 번이면 끝납니다." },
];
export default function ObjectionFaqCta({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: ObjectionFaqCtaProps) {
const [open, setOpen] = useState<boolean[]>([false, false, false]);
const [seen, setSeen] = useState<boolean[]>([false, false, 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 ready = seen.every(Boolean);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const cls = [styles.ob, ready ? styles.isReady : "", demo ? styles.isDemo : ""].join(" ");
function toggle(i: number) {
setOpen(open.map((v, k) => (k === i ? !v : v)));
if (!open[i]) setSeen(seen.map((v, k) => (k === i ? true : v)));
}
return (
<section className={cls} style={vars}>
<p className={styles.title}>
<span className={styles.ic} aria-hidden="true">
<svg viewBox="0 0 256 256" width="18" height="18">
<g fill="currentColor">
<path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2" />
<path d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
</g>
</svg>
</span>
아직 망설이는 부분
</p>
<ul className={styles.list}>
{ITEMS.map((it, i) => {
const itemVars: Vars = { "--i": String(i) };
return (
<li key={it.q} className={`${styles.item} ${open[i] ? styles.isOpen : ""}`} style={itemVars}>
<button
type="button"
className={styles.q}
aria-expanded={open[i]}
aria-controls={`ob-a-${i}`}
onClick={() => toggle(i)}
>
<span>{it.q}</span>
<span className={styles.done}>
<svg className={styles.check} viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth={2} opacity=".35" />
<polyline points="7 12.5 10.5 16 17 8.5" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span>해소됨</span>
</span>
</button>
<div className={styles.a} id={`ob-a-${i}`}>
<div className={styles.aIn}>
<p>{it.a}</p>
</div>
</div>
</li>
);
})}
</ul>
<button type="button" className={styles.cta}>
<span className={styles.ctaT} aria-hidden={ready}>자유롭게 시작하기</span>
<span className={`${styles.ctaT} ${styles.ctaTReady}`} aria-hidden={!ready}>준비됐어요, 눌러주세요</span>
</button>
<p className={styles.status} role="status">{ready ? "모든 걸림돌이 풀렸어요" : ""}</p>
</section>
);
}05 이의 해소 FAQ 와 도달 CTA — 측정 누적면적 40.806% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 랜딩페이지 전환율을 끌어올리는 섹션 9종
이 데모가 속한 글
랜딩페이지 전환율을 끌어올리는 섹션 9종랜딩페이지 전환율이란 방문자가 이메일을 남기거나 예약·구매까지 가는 비율을 말하고, 이 글은 그 전환을 직접 받아내는 섹션 아홉 개를 통째로 주는 코드 모음입니다. 떠나는 방문자를 붙잡는 이탈 팝업부터 읽던 지점에 이 글의 데모 전부 보기


