07離脱意向クーポンポップアップ
カーソルが画面最上部から出ようとした瞬間を検知してクーポンカードが上がり、コードをコピーすると「コピー済み」表示が出て、同一セッションでは二度と開きません。
使いどころ
sessionStoragetranslateYaria-modal

<main class="stage is-demo" id="stage">
<div class="page">
<p class="page__title">週プラン</p>
<p class="page__body">必要な分だけ選んで使えるプランです。<br>週ごとに変更も停止もできます。</p>
<button type="button" class="page__cta" id="ex-cta">プランを見る</button>
<p class="page__hint">お試し: カーソルを画面の上端へ</p>
<p class="page__msg" role="status" id="ex-msg"></p>
</div>
<div class="xp-wrap">
<div class="xp" id="ex-pop" role="dialog" aria-modal="true" aria-label="離脱前クーポン">
<button type="button" class="xp__close" id="ex-close" aria-label="閉じる">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="13" height="13" aria-hidden="true"><g fill="currentColor"><path d="M216 56v144a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16" opacity=".2"/><path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z"/></g></svg>
</button>
<span class="xp__ic" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="26" height="26"><g fill="currentColor"><path d="M200 128a32 32 0 0 0 32 32v32a8 8 0 0 1-8 8H96V56h128a8 8 0 0 1 8 8v32a32 32 0 0 0-32 32" opacity=".2"/><path d="M232 104a8 8 0 0 0 8-8V64a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a8 8 0 0 0 8 8a24 24 0 0 1 0 48a8 8 0 0 0-8 8v32a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16v-32a8 8 0 0 0-8-8a24 24 0 0 1 0-48M32 167.2a40 40 0 0 0 0-78.4V64h56v128H32Zm192 0V192H104V64h120v24.8a40 40 0 0 0 0 78.4"/></g></svg>
</span>
<p class="xp__title">その前に、初月20%OFFクーポン</p>
<p class="xp__code">WELCOME20<span class="xp__sweep" aria-hidden="true"></span></p>
<button type="button" class="xp__copy" id="ex-copy">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="13" height="13" aria-hidden="true"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg>
<span id="ex-copy-t">コードをコピー</span>
<svg class="xp__check" viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><polyline points="4 12 9 17 20 6" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 변수 3개 — 이 데모의 지속시간·이징·포인트색
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-orange);
.stage { position: relative; }
.stage * { box-sizing: border-box; }
// ── 정적 기본 화면: 팝업 없는 축소 페이지가 그대로 보인다 ──
.page { width: min(360px, 94vw); background: $subject-cream; color: $subject-ink; border-radius: $r-card; padding: $sp-5 $sp-4; box-shadow: $shadow-raised; }
.page__title { margin: 0 0 $sp-2; font-weight: 800; font-size: 20px; line-height: 1.2; font-family: inherit; }
.page__body { margin: 0 0 $sp-4; font-size: 12px; line-height: 1.7; color: rgba(23, 20, 26, .72); font-family: inherit; }
.page__cta { border: 0; background: $subject-ink; color: $subject-cream; border-radius: $r-control; padding: $sp-2 $sp-3; font-weight: 700; font-size: 12px; line-height: 1; font-family: inherit; cursor: pointer; }
.page__hint { display: inline-block; margin: $sp-3 0 0; padding: 0 $sp-2; border: 1.5px dashed rgba(23, 20, 26, .4); border-radius: $r-xs; font-size: 10px; line-height: 1.8; color: rgba(23, 20, 26, .6); font-family: inherit; }
.page__msg { margin: $sp-2 0 0; min-height: 13px; font-size: 11px; line-height: 1.3; color: rgba(23, 20, 26, .6); font-family: inherit; }
.page__cta:focus-visible, .xp__close:focus-visible, .xp__copy:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 팝업: 기본은 숨김. 진짜 이탈 감지로 열리면 .is-open ──
.xp-wrap { position: absolute; inset: 0; display: grid; place-items: center; padding: $sp-4; pointer-events: none; }
.xp { display: none; position: relative; pointer-events: auto; width: min(320px, 92vw); background: $subject-cream; color: $subject-ink; border-radius: $r-card; padding: $sp-5 $sp-4 $sp-4; box-shadow: $shadow-raised; text-align: center; }
.xp.is-open { display: block; animation: xp-enter $dur-enter $easing; }
@keyframes xp-enter {
from { transform: translateY(28px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.xp__close { position: absolute; top: $sp-2; right: $sp-2; width: 24px; height: 24px; display: grid; place-items: center; border: 0; border-radius: 50%; background: rgba(23, 20, 26, .08); color: $subject-ink; cursor: pointer; }
.xp__ic { display: grid; place-items: center; margin: 0 auto $sp-2; color: $color; }
.xp__title { margin: 0 0 $sp-3; font-weight: 800; font-size: 14px; line-height: 1.4; font-family: inherit; }
.xp__code { position: relative; display: inline-block; margin: 0 0 $sp-3; padding: $sp-2 $sp-4; border-radius: $r-pill; background: rgba(47, 109, 246, .12); color: $color; font-weight: 800; font-size: 15px; line-height: 1.2; letter-spacing: .08em; font-family: inherit; }
.xp__sweep { position: absolute; left: $sp-4; right: $sp-4; bottom: $sp-1; height: 3px; border-radius: $r-xs; background: $stage-yellow; transform: scaleX(0); transform-origin: left center; } // 규칙8: 좌변서 그어짐
.xp__copy { display: inline-flex; align-items: center; gap: $sp-1; margin: 0; padding: $sp-2 $sp-3; border: 0; border-radius: $r-control; background: $color; color: $stage-paper; font-weight: 800; font-size: 12px; line-height: 1; font-family: inherit; cursor: pointer; }
.xp__check { stroke-dasharray: 28; stroke-dashoffset: 28; transition: stroke-dashoffset $dur-base $easing; }
.xp__copy.is-copied .xp__check { stroke-dashoffset: 0; }
// ── 측정용 자동 루프: 팝업은 .is-demo 에서만 등장하고, 닫힘은 재현하지 않는다 ──
.is-demo .xp { display: block; animation: xp-loop $duration $easing infinite; }
@keyframes xp-loop {
0% { transform: translateY(28px) scale(.94); opacity: 0; }
6% { transform: translateY(10px) scale(.98); opacity: .7; }
12% { transform: translateY(-4px) scale(1.02); opacity: 1; }
18% { transform: translateY(0) scale(1); opacity: 1; }
100% { transform: translateY(0) scale(1); opacity: 1; }
}
.is-demo .xp__sweep { animation: xp-sweep $duration $ease-out infinite; }
@keyframes xp-sweep {
0%, 24% { transform: scaleX(0); }
46%, 100% { transform: scaleX(1); }
}
.is-demo .xp__ic { animation: xp-bob $duration $ease-spring infinite; }
@keyframes xp-bob {
0%, 46%, 100% { transform: translateY(0) rotate(0deg); }
54% { transform: translateY(-5px) rotate(-7deg); }
62% { transform: translateY(0) rotate(0deg); }
}
.is-demo .xp__copy { animation: xp-pop $duration $easing infinite; }
@keyframes xp-pop {
0%, 70%, 100% { transform: scale(1); }
78% { transform: scale(1.08); }
86% { transform: scale(1); }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.xp, .is-demo .xp, .is-demo .xp__sweep, .is-demo .xp__ic, .is-demo .xp__copy, .xp__check {
animation: none !important; transition: none !important; transform: none !important; opacity: 1 !important;
}
.is-demo .xp__sweep { transform: scaleX(1) !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.xp-wrap { padding: $sp-2; }
.page { width: min(288px, 92vw); padding: $sp-3 $sp-3 $sp-4; }
.page__title { margin: 0 0 $sp-1; font-size: 15px; }
.page__body { margin: 0 0 $sp-2; font-size: 10px; line-height: 1.5; }
.page__cta { padding: $sp-1 $sp-2; font-size: 10px; }
.page__hint { margin: $sp-2 0 0; font-size: 9px; line-height: 1.6; }
.page__msg { margin: $sp-1 0 0; min-height: 0; font-size: 9px; }
.xp { width: min(268px, 92vw); padding: $sp-3 $sp-3; }
.xp__ic { margin: 0 auto $sp-1; }
.xp__ic svg { width: 18px; height: 18px; }
.xp__title { margin: 0 0 $sp-2; font-size: 11px; line-height: 1.3; }
.xp__code { margin: 0 0 $sp-2; padding: $sp-1 $sp-3; font-size: 12px; }
.xp__sweep { left: $sp-3; right: $sp-3; bottom: $sp-1; height: 2px; }
.xp__copy { padding: $sp-1 $sp-2; font-size: 10px; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.stage * {
box-sizing: border-box;
}
.page {
width: min(360px, 94vw);
background: #fff7e6;
color: #17141a;
border-radius: 16px;
padding: 20px 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.page__title {
margin: 0 0 8px;
font-weight: 800;
font-size: 20px;
line-height: 1.2;
font-family: inherit;
}
.page__body {
margin: 0 0 16px;
font-size: 12px;
line-height: 1.7;
color: rgba(23, 20, 26, 0.72);
font-family: inherit;
}
.page__cta {
border: 0;
background: #17141a;
color: #fff7e6;
border-radius: 12px;
padding: 8px 12px;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
cursor: pointer;
}
.page__hint {
display: inline-block;
margin: 12px 0 0;
padding: 0 8px;
border: 1.5px dashed rgba(23, 20, 26, 0.4);
border-radius: 4px;
font-size: 10px;
line-height: 1.8;
color: rgba(23, 20, 26, 0.6);
font-family: inherit;
}
.page__msg {
margin: 8px 0 0;
min-height: 13px;
font-size: 11px;
line-height: 1.3;
color: rgba(23, 20, 26, 0.6);
font-family: inherit;
}
.page__cta:focus-visible, .xp__close:focus-visible, .xp__copy:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.xp-wrap {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 16px;
pointer-events: none;
}
.xp {
display: none;
position: relative;
pointer-events: auto;
width: min(320px, 92vw);
background: #fff7e6;
color: #17141a;
border-radius: 16px;
padding: 20px 16px 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
text-align: center;
}
.xp.is-open {
display: block;
animation: xp-enter 600ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
@keyframes xp-enter {
from {
transform: translateY(28px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.xp__close {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: rgba(23, 20, 26, 0.08);
color: #17141a;
cursor: pointer;
}
.xp__ic {
display: grid;
place-items: center;
margin: 0 auto 8px;
color: #2f6df6;
}
.xp__title {
margin: 0 0 12px;
font-weight: 800;
font-size: 14px;
line-height: 1.4;
font-family: inherit;
}
.xp__code {
position: relative;
display: inline-block;
margin: 0 0 12px;
padding: 8px 16px;
border-radius: 999px;
background: rgba(47, 109, 246, 0.12);
color: #2f6df6;
font-weight: 800;
font-size: 15px;
line-height: 1.2;
letter-spacing: 0.08em;
font-family: inherit;
}
.xp__sweep {
position: absolute;
left: 16px;
right: 16px;
bottom: 4px;
height: 3px;
border-radius: 4px;
background: #ffd23f;
transform: scaleX(0);
transform-origin: left center;
}
.xp__copy {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
padding: 8px 12px;
border: 0;
border-radius: 12px;
background: #2f6df6;
color: #ffffff;
font-weight: 800;
font-size: 12px;
line-height: 1;
font-family: inherit;
cursor: pointer;
}
.xp__check {
stroke-dasharray: 28;
stroke-dashoffset: 28;
transition: stroke-dashoffset 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.xp__copy.is-copied .xp__check {
stroke-dashoffset: 0;
}
.is-demo .xp {
display: block;
animation: xp-loop 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes xp-loop {
0% {
transform: translateY(28px) scale(0.94);
opacity: 0;
}
6% {
transform: translateY(10px) scale(0.98);
opacity: 0.7;
}
12% {
transform: translateY(-4px) scale(1.02);
opacity: 1;
}
18% {
transform: translateY(0) scale(1);
opacity: 1;
}
100% {
transform: translateY(0) scale(1);
opacity: 1;
}
}
.is-demo .xp__sweep {
animation: xp-sweep 2s ease-out infinite;
}
@keyframes xp-sweep {
0%, 24% {
transform: scaleX(0);
}
46%, 100% {
transform: scaleX(1);
}
}
.is-demo .xp__ic {
animation: xp-bob 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes xp-bob {
0%, 46%, 100% {
transform: translateY(0) rotate(0deg);
}
54% {
transform: translateY(-5px) rotate(-7deg);
}
62% {
transform: translateY(0) rotate(0deg);
}
}
.is-demo .xp__copy {
animation: xp-pop 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes xp-pop {
0%, 70%, 100% {
transform: scale(1);
}
78% {
transform: scale(1.08);
}
86% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.xp, .is-demo .xp, .is-demo .xp__sweep, .is-demo .xp__ic, .is-demo .xp__copy, .xp__check {
animation: none !important;
transition: none !important;
transform: none !important;
opacity: 1 !important;
}
.is-demo .xp__sweep {
transform: scaleX(1) !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.xp-wrap {
padding: 8px;
}
.page {
width: min(288px, 92vw);
padding: 12px 12px 16px;
}
.page__title {
margin: 0 0 4px;
font-size: 15px;
}
.page__body {
margin: 0 0 8px;
font-size: 10px;
line-height: 1.5;
}
.page__cta {
padding: 4px 8px;
font-size: 10px;
}
.page__hint {
margin: 8px 0 0;
font-size: 9px;
line-height: 1.6;
}
.page__msg {
margin: 4px 0 0;
min-height: 0;
font-size: 9px;
}
.xp {
width: min(268px, 92vw);
padding: 12px 12px;
}
.xp__ic {
margin: 0 auto 4px;
}
.xp__ic svg {
width: 18px;
height: 18px;
}
.xp__title {
margin: 0 0 8px;
font-size: 11px;
line-height: 1.3;
}
.xp__code {
margin: 0 0 8px;
padding: 4px 12px;
font-size: 12px;
}
.xp__sweep {
left: 12px;
right: 12px;
bottom: 4px;
height: 2px;
}
.xp__copy {
padding: 4px 8px;
font-size: 10px;
}
}// 실제 동작: 마우스가 화면 맨 위로 벗어나면(mouseleave, clientY < 12) 쿠폰 팝업이 열린다. 복사·閉じる·Escape, 그리고 한 번 떴으면 이 세션(sessionStorage exit271)엔 다시 안 뜨는 것까지 전부 진짜로 작동한다.
var stage = document.getElementById('stage');
var pop = document.getElementById('ex-pop');
var msg = document.getElementById('ex-msg');
var closeBtn = document.getElementById('ex-close');
var cta = document.getElementById('ex-cta');
var copyBtn = document.getElementById('ex-copy');
var copyT = document.getElementById('ex-copy-t');
var CODE = 'WELCOME20';
var MICRO = 'このセッションでは再表示しません';
function seen() { try { return sessionStorage.getItem('exit271') === '1'; } catch (err) { return false; } }
function openPop() {
stage.classList.remove('is-demo');
pop.classList.add('is-open');
try { sessionStorage.setItem('exit271', '1'); } catch (err) {}
closeBtn.focus({preventScroll:true});
}
function closePop() {
pop.classList.remove('is-open');
msg.textContent = MICRO;
cta.focus({preventScroll:true});
}
if (seen()) msg.textContent = MICRO;
document.addEventListener('mouseleave', function (e) {
if (e.clientY < 12 && !pop.classList.contains('is-open')) {
if (seen()) { msg.textContent = MICRO; return; }
openPop();
}
});
closeBtn.addEventListener('click', closePop);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && pop.classList.contains('is-open')) closePop();
});
copyBtn.addEventListener('click', function () {
var fail = function () { msg.textContent = '手動でコピー: ' + CODE; };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(CODE).then(function () {
copyT.textContent = 'コピー済み';
copyBtn.classList.add('is-copied');
}, fail);
} else { fail(); }
});
function human() { stage.classList.remove('is-demo'); }
document.addEventListener('pointerdown', human, { once: true });
document.addEventListener('keydown', human, { once: true });import { useEffect, useRef, useState } from "react";
import styles from "./ExitIntentCoupon.module.scss";
// 이벤트 타입은 이 파일 안에서 최소한으로 선언한다 (외부 제네릭에 기대지 않는다)
type MouseEvt = { clientY: number };
type KeyEvt = { key: string };
type Box<T> = { current: T | null };
export type ExitIntentCouponProps = { duration?: number; easing?: string; color?: string };
type Vars = { "--duration": string; "--easing": string; "--color": string };
const CODE = "WELCOME20";
const MICRO = "이 세션엔 다시 안 떠요";
export default function ExitIntentCoupon({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: ExitIntentCouponProps) {
const [demo, setDemo] = useState(true);
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [msg, setMsg] = useState("");
const closeRef: Box<HTMLButtonElement> = useRef<HTMLButtonElement>(null);
const ctaRef: Box<HTMLButtonElement> = useRef<HTMLButtonElement>(null);
const vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as Vars;
function seen(): boolean {
try { return sessionStorage.getItem("exit271") === "1"; } catch (err) { return false; }
}
useEffect(() => {
if (seen()) setMsg(MICRO);
function onLeave(e: MouseEvt) {
if (e.clientY >= 12 || open) return;
if (seen()) { setMsg(MICRO); return; }
try { sessionStorage.setItem("exit271", "1"); } catch (err) {}
setDemo(false);
setOpen(true);
}
function onKey(e: KeyEvt) {
if (e.key === "Escape") close();
}
document.addEventListener("mouseleave", onLeave);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mouseleave", onLeave);
document.removeEventListener("keydown", onKey);
};
}, [open]);
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);
};
}, []);
useEffect(() => {
if (open && closeRef.current) closeRef.current.focus({ preventScroll: true });
}, [open]);
function close() {
setOpen(false);
setMsg(MICRO);
if (ctaRef.current) ctaRef.current.focus({ preventScroll: true });
}
function copy() {
const fail = () => setMsg(`직접 복사: ${CODE}`);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(CODE).then(() => setCopied(true), fail);
} else { fail(); }
}
return (
<main className={`${styles.stage} ${demo ? styles.demo : ""}`} style={vars}>
<div className={styles.page}>
<p className={styles.pageTitle}>주간 요금제</p>
<p className={styles.pageBody}>필요한 만큼만 골라 쓰는 요금제예요.<br />주간 단위로 바꾸고 그만둘 수 있어요.</p>
<button type="button" className={styles.pageCta} ref={ctaRef}>요금제 자세히 보기</button>
<p className={styles.pageHint}>시험: 커서를 화면 맨 위로</p>
<p className={styles.pageMsg} role="status">{msg}</p>
</div>
<div className={styles.wrap}>
<div
className={`${styles.xp} ${open ? styles.xpOpen : ""}`}
role="dialog"
aria-modal="true"
aria-label="떠나기 전 쿠폰"
>
<button type="button" className={styles.close} aria-label="닫기" ref={closeRef} onClick={close}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="13" height="13" aria-hidden="true"><g fill="currentColor"><path d="M216 56v144a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16" opacity=".2" /><path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" /></g></svg>
</button>
<span className={styles.ic} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="26" height="26"><g fill="currentColor"><path d="M200 128a32 32 0 0 0 32 32v32a8 8 0 0 1-8 8H96V56h128a8 8 0 0 1 8 8v32a32 32 0 0 0-32 32" opacity=".2" /><path d="M232 104a8 8 0 0 0 8-8V64a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a8 8 0 0 0 8 8a24 24 0 0 1 0 48a8 8 0 0 0-8 8v32a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16v-32a8 8 0 0 0-8-8a24 24 0 0 1 0-48M32 167.2a40 40 0 0 0 0-78.4V64h56v128H32Zm192 0V192H104V64h120v24.8a40 40 0 0 0 0 78.4" /></g></svg>
</span>
<p className={styles.title}>가기 전에, 첫 달 20% 쿠폰</p>
<p className={styles.code}>{CODE}<span className={styles.sweep} aria-hidden="true" /></p>
<button type="button" className={`${styles.copy} ${copied ? styles.copied : ""}`} onClick={copy}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="13" height="13" aria-hidden="true"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2" /><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" /></g></svg>
<span>{copied ? "복사됨" : "코드 복사"}</span>
<svg className={styles.check} viewBox="0 0 24 24" width="13" height="13" aria-hidden="true">
<polyline points="4 12 9 17 20 6" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
</div>
</main>
);
}07 離脱意向クーポンポップアップ — 計測 累積面積 23.675% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · ランディングページ 改善に直結するセクション9選
このデモが属する記事
ランディングページ 改善に直結するセクション9選ランディングページ 改善とは、訪問者がメールアドレスを残す・予約する・購入するという形で目的地まで進む割合を上げる作業です。この記事はその転換を直接受けるセクションを9つ、コードごと配ります。離脱しかけを引き留めるクーポンポップアップから、 この記事のデモをすべて見る


