08Win-back email
Three stat cells roll from zero up to their real values and the button rises below them, while the unsubscribe link stays small on the same screen.
Best forReactivating dormant users, subscription renewal notices
requestAnimationFrametranslateYprefers-color-scheme

<main class="stage">
<div class="mk is-demo" id="mk">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="mk__table">
<tr><td align="center" class="mk__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="mk__card" style="max-width:400px">
<tr><td class="mk__brand">◆ Acme</td></tr>
<tr><td align="center" class="mk__hero">
<span class="mk__badge" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="mk__ico"><path d="M224,64V208H32V48H208A16,16,0,0,1,224,64Z" opacity="0.2"/><path d="M232,208a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V48a8,8,0,0,1,16,0V156.69l50.34-50.35a8,8,0,0,1,11.32,0L128,132.69,180.69,80H160a8,8,0,0,1,0-16h40a8,8,0,0,1,8,8v40a8,8,0,0,1-16,0V91.31l-58.34,58.35a8,8,0,0,1-11.32,0L96,123.31l-56,56V200H224A8,8,0,0,1,232,208Z"/></svg></span>
</td></tr>
<tr><td align="center" class="mk__title">Welcome back! Here is what you missed</td></tr>
<tr><td class="mk__stats">
<div class="mk__stat">
<span class="mk__num" data-target="1280">0</span>
<span class="mk__statlabel">New neighbors joined</span>
</div>
<div class="mk__stat">
<span class="mk__num" data-target="36">0</span>
<span class="mk__statlabel">New posts</span>
</div>
<div class="mk__stat">
<span class="mk__num" data-target="14">0</span>
<span class="mk__statlabel">Days away</span>
</div>
</td></tr>
<tr><td align="center" class="mk__ctawrap"><a href="#" role="button" class="mk__cta">Start again</a></td></tr>
<tr><td align="center" class="mk__footer">Acme Inc · Seoul · <a href="#" class="mk__unsub">Unsubscribe</a></td></tr>
</table>
</td></tr>
</table>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-blue-deep;
@include stage($stage-paper);
.stage { position: relative; }
.mk { width: min(100%, 400px); }
.mk__table { border-collapse: collapse; }
.mk__wrap { padding: 0; }
.mk__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; border: 1px solid rgba(23,20,26,.1); overflow: hidden;
}
.mk__card td { padding: $sp-1 $sp-5; font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; -webkit-text-size-adjust: 100%; line-height: 1; }
.mk__brand { font-weight: 800; font-size: 11px; line-height: 1; color: rgba(23,20,26,.62); letter-spacing: .02em; padding-top: $sp-2 !important; }
.mk__hero { padding-top: $sp-1 !important; padding-bottom: 0 !important; }
.mk__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px); border-radius: $r-sm;
background: #0a4fe8; color: $subject-cream;
}
.mk__ico { width: 62%; height: 62%; }
.mk__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-2 !important; }
.mk__stats {
display: grid; grid-template-columns: repeat(3, 1fr); gap: $sp-2;
padding-top: 0 !important; padding-bottom: $sp-1 !important;
}
.mk__stat {
display: flex; flex-direction: column; align-items: center; gap: $sp-1;
background: rgba(47,109,246,.06); border-radius: $r-sm; padding: $sp-2 $sp-1;
}
.mk__num { font-weight: 800; font-size: clamp(14px, 4.6vw, 19px); line-height: 1; color: $subject-blue; font-variant-numeric: tabular-nums; }
.mk__statlabel { font-weight: 600; font-size: 8px; line-height: 1.3; color: rgba(23,20,26,.62); text-align: center; }
.mk__ctawrap { padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.mk__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit; color: $subject-cream; text-decoration: none; border-radius: $r-control;
background: #7c90be; // CTA 파랑을 어둡게 — 크림 글자 4.25:1 미달이라 5.29:1 로 (279 선례)
}
.mk__footer { font-weight: 500; font-size: 8px; line-height: 1.4; color: rgba(23,20,26,.62); padding-top: $sp-1 !important; padding-bottom: $sp-2 !important; }
.mk__footer a { color: inherit; }
// ── 다크 모드: 시스템 설정을 따라가는 판 (prefers-color-scheme) — 메일 앱 다크 테마 흉내
@media (prefers-color-scheme: dark) {
.mk__card { background: $stage-ink; border-color: rgba(255,247,230,.14); }
.mk__brand { color: rgba(255,247,230,.7); }
.mk__title { color: $subject-cream; }
.mk__stat { background: rgba(255,247,230,.08); }
.mk__num { color: $subject-sky; }
.mk__statlabel { color: rgba(255,247,230,.55); }
.mk__footer { color: rgba(255,247,230,.4); }
}
// ── 미리보기 루프: 통계 칸이 살짝 떠오르고 CTA 가 숨쉰다 (2s 주기, 복귀 86%)
// 숫자 굴러 오르기(0→값)는 requestAnimationFrame 스크립트가 담당 — CSS 시계 밖 실시간
.mk.is-demo .mk__stat { animation-name: rgRise; animation-duration: 2s; animation-timing-function: $easing; animation-iteration-count: infinite; }
.mk.is-demo .mk__stat:nth-child(2) { animation-delay: 100ms; }
.mk.is-demo .mk__stat:nth-child(3) { animation-delay: 200ms; }
.mk.is-demo .mk__cta { animation-name: rgBreath; animation-duration: 2s; animation-timing-function: $ease-out; animation-iteration-count: infinite; }
@keyframes rgRise {
0% { opacity: 0; transform: translateY(10px); }
26% { opacity: 1; transform: translateY(0); }
82% { opacity: 1; transform: translateY(0); }
86% { opacity: 0; transform: translateY(6px); }
100% { opacity: 0; transform: translateY(10px); }
}
@keyframes rgBreath {
0%, 12% { opacity: .3; transform: translateY(8px); }
44% { opacity: 1; transform: translateY(0); }
60% { transform: translateY(0) scale(1.03); }
76% { opacity: 1; transform: translateY(0) scale(1); }
84%, 100% { opacity: .3; transform: translateY(8px) scale(1); }
}
// ── 320×200 폰 실측 대비 압축 (BD9)
@media (max-width: 340px), (max-height: 220px) {
.mk__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.mk__brand { padding-top: 0 !important; font-size: 9px !important; }
.mk__badge { width: 22px; height: 22px; }
.mk__title { font-size: 11px !important; padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.mk__num { font-size: 13px !important; }
.mk__statlabel { font-size: 7px !important; }
.mk__stat { padding: $sp-1 0 !important; }
.mk__cta { padding: 0 $sp-5 !important; font-size: 10px !important; }
.mk__footer { font-size: 7px !important; padding-top: 0 !important; padding-bottom: $sp-1 !important; }
}
@media (prefers-reduced-motion: reduce) {
.mk.is-demo .mk__stat,
.mk.is-demo .mk__cta { animation: none !important; opacity: 1; transform: none; }
}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);
}
.stage {
position: relative;
}
.mk {
width: min(100%, 400px);
}
.mk__table {
border-collapse: collapse;
}
.mk__wrap {
padding: 0;
}
.mk__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
border: 1px solid rgba(23, 20, 26, 0.1);
overflow: hidden;
}
.mk__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.mk__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
color: rgba(23, 20, 26, 0.62);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.mk__hero {
padding-top: 4px !important;
padding-bottom: 0 !important;
}
.mk__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 8px;
background: #0a4fe8;
color: #fff7e6;
}
.mk__ico {
width: 62%;
height: 62%;
}
.mk__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 8px !important;
}
.mk__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.mk__stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: rgba(47, 109, 246, 0.06);
border-radius: 8px;
padding: 8px 4px;
}
.mk__num {
font-weight: 800;
font-size: clamp(14px, 4.6vw, 19px);
line-height: 1;
color: #2f6df6;
font-variant-numeric: tabular-nums;
}
.mk__statlabel {
font-weight: 600;
font-size: 8px;
line-height: 1.3;
color: rgba(23, 20, 26, 0.62);
text-align: center;
}
.mk__ctawrap {
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.mk__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #fff7e6;
text-decoration: none;
border-radius: 12px;
background: #7c90be;
}
.mk__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
color: rgba(23, 20, 26, 0.62);
padding-top: 4px !important;
padding-bottom: 8px !important;
}
.mk__footer a {
color: inherit;
}
@media (prefers-color-scheme: dark) {
.mk__card {
background: #17141a;
border-color: rgba(255, 247, 230, 0.14);
}
.mk__brand {
color: rgba(255, 247, 230, 0.7);
}
.mk__title {
color: #fff7e6;
}
.mk__stat {
background: rgba(255, 247, 230, 0.08);
}
.mk__num {
color: #6ec8ff;
}
.mk__statlabel {
color: rgba(255, 247, 230, 0.55);
}
.mk__footer {
color: rgba(255, 247, 230, 0.4);
}
}
.mk.is-demo .mk__stat {
animation-name: rgRise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.mk.is-demo .mk__stat:nth-child(2) {
animation-delay: 100ms;
}
.mk.is-demo .mk__stat:nth-child(3) {
animation-delay: 200ms;
}
.mk.is-demo .mk__cta {
animation-name: rgBreath;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes rgRise {
0% {
opacity: 0;
transform: translateY(10px);
}
26% {
opacity: 1;
transform: translateY(0);
}
82% {
opacity: 1;
transform: translateY(0);
}
86% {
opacity: 0;
transform: translateY(6px);
}
100% {
opacity: 0;
transform: translateY(10px);
}
}
@keyframes rgBreath {
0%, 12% {
opacity: 0.3;
transform: translateY(8px);
}
44% {
opacity: 1;
transform: translateY(0);
}
60% {
transform: translateY(0) scale(1.03);
}
76% {
opacity: 1;
transform: translateY(0) scale(1);
}
84%, 100% {
opacity: 0.3;
transform: translateY(8px) scale(1);
}
}
@media (max-width: 340px), (max-height: 220px) {
.mk__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.mk__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.mk__badge {
width: 22px;
height: 22px;
}
.mk__title {
font-size: 11px !important;
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.mk__num {
font-size: 13px !important;
}
.mk__statlabel {
font-size: 7px !important;
}
.mk__stat {
padding: 4px 0 !important;
}
.mk__cta {
padding: 0 20px !important;
font-size: 10px !important;
}
.mk__footer {
font-size: 7px !important;
padding-top: 0 !important;
padding-bottom: 4px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.mk.is-demo .mk__stat,
.mk.is-demo .mk__cta {
animation: none !important;
opacity: 1;
transform: none;
}
}var counters = document.querySelectorAll('.mk__num');
var DURATION = 1100;
for (var c = 0; c < counters.length; c++) {
counters[c].setAttribute('data-start', String(performance.now()));
}
function rollNumber(now) {
var allDone = true;
for (var i = 0; i < counters.length; i++) {
var el = counters[i];
var target = parseInt(el.getAttribute('data-target'), 10);
var started = parseFloat(el.getAttribute('data-start'));
var t = Math.min(1, (now - started) / DURATION);
var eased = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(target * eased).toLocaleString('ko-KR');
if (t < 1) { allDone = false; }
}
if (!allDone) { requestAnimationFrame(rollNumber); }
}
requestAnimationFrame(rollNumber);import { useEffect, useRef, useState } from "react";
import styles from "./WinbackStats.module.scss";
export type WinbackStatsProps = { duration?: number; easing?: string; color?: string };
const STATS = [
{ target: 1280, label: "새로 가입한 이웃" },
{ target: 36, label: "새로 올라온 글" },
{ target: 14, label: "떠나 있던 날" },
];
export default function WinbackStats({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#0a4fe8" }: WinbackStatsProps) {
const [demo, setDemo] = useState(true);
const [vals, setVals] = useState<number[]>([0, 0, 0]);
const startRef = useRef<number | null>(null);
const rafRef = useRef<number | null>(null);
const rollMs = 1100;
useEffect(() => {
const roll = (now: number) => {
if (startRef.current === null) { startRef.current = now; }
const t = Math.min(1, (now - startRef.current) / rollMs);
const eased = 1 - Math.pow(1 - t, 3);
setVals(STATS.map((s) => Math.round(s.target * eased)));
if (t < 1) { rafRef.current = requestAnimationFrame(roll); }
};
rafRef.current = requestAnimationFrame(roll);
return () => { if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); } };
}, []);
return (
<main className={styles.stage}>
<div className={styles.mk}>
<div className={styles.card}>
<div className={styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={styles.badge} style={{ background: color }} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" className={styles.ico}><path d="M224,64V208H32V48H208A16,16,0,0,1,224,64Z" opacity="0.2"/><path d="M232,208a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V48a8,8,0,0,1,16,0V156.69l50.34-50.35a8,8,0,0,1,11.32,0L128,132.69,180.69,80H160a8,8,0,0,1,0-16h40a8,8,0,0,1,8,8v40a8,8,0,0,1-16,0V91.31l-58.34,58.35a8,8,0,0,1-11.32,0L96,123.31l-56,56V200H224A8,8,0,0,1,232,208Z"/></svg>
</span>
</div>
<div className={styles.title}>돌아오셨네요! 그동안 있었던 일</div>
<div className={styles.stats}>
{STATS.map((s, i) => (
<div key={s.label} className={styles.stat} style={demo ? { animationName: "rgRise", animationDuration: "2s", animationTimingFunction: easing, animationIterationCount: "infinite", animationDelay: `${i * 100}ms` } : undefined}>
<span className={styles.num} style={{ color: color }}>{vals[i].toLocaleString("ko-KR")}</span>
<span className={styles.statlabel}>{s.label}</span>
</div>
))}
</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2a5fd8", ...(demo ? { animationName: "rgBreath", animationDuration: "2s", animationTimingFunction: easing, animationIterationCount: "infinite" } : {}) }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); setDemo(false); }}>다시 시작하기</a>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#" className={styles.unsub}>수신거부</a></div>
</div>
</div>
</main>
);
}08 Win-back email — Measured area 4.815% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Marketing Email Design: 9 Templates, Outlook-Safe
The post this demo lives in
Marketing Email Design: 9 Templates, Outlook-SafeMarketing email design shapes the emails you send to sell — newsletters, coupons, countdowns, reminders. These nine are See all demos in this post


