이메일 템플릿 디자인 9종 — 웰컴부터 보안 알림까지
이메일 템플릿 디자인은 완성된 뉴스레터 하나를 예쁘게 꾸미는 일이 아니라, 웰컴·영수증·비밀번호 재설정처럼 서비스가 실제로 자동 발송하는 아홉 가지 "거래형" 메일 종류별로 나눠 만드는 일입니다.
01 welcome
02 receipt
03 password-reset
04 notification
05 shipping
06 email-verification
07 invoice
08 appointment-confirmation
09 security-alert
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 웰컴 이메일
- 02 영수증 · 주문 확인 이메일
- 03 비밀번호 재설정 이메일
- 04 알림 이메일
- 05 배송 안내 이메일
- 06 이메일 인증 코드 메일
- 07 청구서 · 구독 결제 이메일
- 08 예약 확인 이메일
- 09 보안 알림 이메일
아홉 개는 인기순이 아니라 한 사용자가 서비스를 겪는 순서대로 놓았습니다. 가입 직후 받는 웰컴 메일, 첫 결제의 영수증, 계정을 잊었을 때의 비밀번호 재설정과 인증 코드, 평소에 오는 알림, 물건을 샀을 때의 배송 안내, 매달 오는 청구서, 예약을 잡았을 때의 확인 메일, 마지막으로 보안이 걱정될 때의 로그인 경고 순입니다. 아홉 개 전부 표 기반 레이아웃(<table role="presentation">)과 max-width: 400px 유동형 카드로 짜여 있어, 무료 이메일 템플릿을 고쳐 쓸 때보다 실제 이메일 서비스(ESP)에 붙이기가 쉽습니다. 같은 표 뼈대로 정기 발송을 겨냥한 편이 뉴스레터 디자인 9종이고, 도장·돌림판처럼 혜택을 즐기는 쪽이 쿠폰 디자인 9종입니다.
01웰컴 이메일
웰컴 이메일에서 무엇이 보이나
가입 직후 자동으로 나가는 첫 이메일입니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">👋</span></td></tr>
<tr><td align="center" class="ema__title">환영합니다, 지훈님!</td></tr>
<tr><td align="center" class="ema__body">가입해주셔서 감사합니다.<br>시작하려면 아래 버튼을 누르세요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">시작하기</a></td>
</tr></table>
</td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge { animation: ema-bounce 2s ease-in-out infinite; }
@keyframes ema-bounce { 0%, 15% { transform: translateY(0) scale(1); } 30% { transform: translateY(-7px) scale(1.06); } 45%, 65% { transform: translateY(0) scale(1); } 100% { transform: translateY(0) scale(1); } }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}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);
}
.stage {
position: relative;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge {
animation: ema-bounce 2s ease-in-out infinite;
}
@keyframes ema-bounce {
0%, 15% {
transform: translateY(0) scale(1);
}
30% {
transform: translateY(-7px) scale(1.06);
}
45%, 65% {
transform: translateY(0) scale(1);
}
100% {
transform: translateY(0) scale(1);
}
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
cta.addEventListener('click', function (e) {
e.preventDefault();
ema.classList.remove('is-demo');
var on = ema.classList.toggle('is-done');
cta.setAttribute('aria-pressed', String(on));
cta.textContent = on ? '시작했어요 ✓' : '시작하기';
});import { useState, useEffect, useRef } from "react";
import styles from "./Welcome.module.scss";
export type WelcomeProps = { duration?: number; easing?: string; color?: string };
export default function Welcome({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: WelcomeProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [done, setDone] = useState(false);
return (
<main className={styles.stage} style={{ background: "#17141a" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#2f6df6", animationName: "bounce", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} aria-hidden="true">👋</span>
</div>
<div className={styles.title}>환영합니다, 지훈님!</div>
<div className={styles.body}>가입해주셔서 감사합니다.<br/>시작하려면 아래 버튼을 누르세요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); setDone((v) => !v); }}>{done ? "시작했어요 ✓" : "시작하기"}</a>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}01 · 웰컴 이메일 — 측정 누적면적 0.631% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
배지가 2초마다 살짝 튀어 오르며 첫인상을 만들고, 버튼을 누르면 자리에서 바로 "시작했어요 ✓"로 라벨이 바뀝니다.
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge">👋</span></td></tr>
<tr><td align="center" class="ema__title">환영합니다, 지훈님!</td></tr>
<tr><td align="center" class="ema__ctawrap">
<a href="#" role="button" class="ema__cta" id="cta">시작하기</a>
</td></tr>
</table>
02영수증 · 주문 확인 이메일
화면에서 일어나는 일
결제 직후 자동 발송되는 주문 확인 메일입니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">🧾</span></td></tr>
<tr><td align="center" class="ema__title">주문이 확인됐어요</td></tr>
<tr><td align="center" class="ema__body">주문번호 #48213 결제가 완료됐어요.<br>아래에서 영수증을 확인하세요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">영수증 보기</a></td>
</tr></table>
</td></tr>
<tr><td class="ema__detailrow"><button type="button" class="ema__detailtoggle" id="detailtoggle" aria-expanded="false" aria-controls="detail">주문 내역 2건 · 총 32,800원</button><div class="ema__detail" id="detail"><div class="ema__detailin"><div class="ema__li"><span>기본 티셔츠 · 1개</span><span>18,900원</span></div><div class="ema__li"><span>양말 세트 · 1개</span><span>13,900원</span></div></div></div></td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-yellow);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
// 다크 카드 위 추가 줄 — 밝은 모드의 rgba($subject-ink,.72) 가 그대로 남으면 잉크 면 위 잉크 글자(1.00:1)라 안 보인다 (2026-09-13 대비 스캔)
.ema.is-dark .ema__detailtoggle, .ema.is-dark .ema__li { color: rgba(255,247,230,.72); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
.ema__detailtoggle, .ema__li { color: rgba(255,247,230,.72); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema__badge { position: relative; overflow: hidden; }
.ema.is-demo .ema__badge::after {
content: ""; position: absolute; inset: 0; border-radius: 50%;
background: linear-gradient(120deg, transparent 30%, rgba(255,255,255,.55) 50%, transparent 70%);
transform: translateX(-120%);
}
.ema.is-demo .ema__badge::after { animation: ema-sweep 2s linear infinite; }
@keyframes ema-sweep { 0% { transform: translateX(-120%); } 90%, 100% { transform: translateX(120%); } }
.ema__detailrow { padding-top: 0 !important; }
.ema__detailtoggle {
width: 100%; text-align: left; background: none; border: none; cursor: pointer;
font-weight: 600; font-size: 12px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding: $sp-1 0;
}
.ema__detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows $dur-base $ease-spring; }
.ema__detailin { overflow: hidden; }
.ema.is-open .ema__detail { grid-template-rows: 1fr; }
.ema__li { display: flex; justify-content: space-between; font-weight: 500; font-size: 11px; line-height: 1.6;
font-family: inherit; color: rgba($subject-ink, .72); padding: $sp-1 0; border-top: 1px solid rgba(0,0,0,.06); }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema.is-dark .ema__detailtoggle, .ema.is-dark .ema__li {
color: rgba(255, 247, 230, 0.72);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema__detailtoggle, .ema__li {
color: rgba(255, 247, 230, 0.72);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema__badge {
position: relative;
overflow: hidden;
}
.ema.is-demo .ema__badge::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(120deg, transparent 30%, rgba(255, 255, 255, 0.55) 50%, transparent 70%);
transform: translateX(-120%);
}
.ema.is-demo .ema__badge::after {
animation: ema-sweep 2s linear infinite;
}
@keyframes ema-sweep {
0% {
transform: translateX(-120%);
}
90%, 100% {
transform: translateX(120%);
}
}
.ema__detailrow {
padding-top: 0 !important;
}
.ema__detailtoggle {
width: 100%;
text-align: left;
background: none;
border: none;
cursor: pointer;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding: 4px 0;
}
.ema__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ema__detailin {
overflow: hidden;
}
.ema.is-open .ema__detail {
grid-template-rows: 1fr;
}
.ema__li {
display: flex;
justify-content: space-between;
font-weight: 500;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding: 4px 0;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
var dt = document.getElementById('detailtoggle');
dt.addEventListener('click', function () {
ema.classList.remove('is-demo');
var open = ema.classList.toggle('is-open');
dt.setAttribute('aria-expanded', String(open));
});import { useState, useEffect, useRef } from "react";
import styles from "./Receipt.module.scss";
export type ReceiptProps = { duration?: number; easing?: string; color?: string };
export default function Receipt({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: ReceiptProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
return (
<main className={styles.stage} style={{ background: "#ffd23f" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#2f6df6", animationName: "flash", animationDuration: "2s", animationTimingFunction: "linear", animationIterationCount: "infinite" }} aria-hidden="true">🧾</span>
</div>
<div className={styles.title}>주문이 확인됐어요</div>
<div className={styles.body}>주문번호 #48213 결제가 완료됐어요.<br/>아래에서 영수증을 확인하세요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); }}>{"영수증 보기"}</a>
</div>
<div className={styles.row}>
<button type="button" className={styles.detailtoggle} aria-expanded={open} onClick={() => setOpen((v) => !v)}>주문 내역 2건 · 총 32,800원</button>
<div className={`${styles.detail} ${open ? styles.open : ""}`}><div className={styles.detailin}>
<div className={styles.li}><span>기본 티셔츠 · 1개</span><span>18,900원</span></div>
<div className={styles.li}><span>양말 세트 · 1개</span><span>13,900원</span></div>
</div></div>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}02 · 영수증 · 주문 확인 이메일 — 측정 누적면적 0.637% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드 읽는 법
"영수증 보기"를 누르면 grid-template-rows가 0fr에서 1fr로 바뀌면서 주문 내역 2건이 실제로 펼쳐집니다.
.ema__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $dur-base $ease-spring;
}
.ema.is-open .ema__detail { grid-template-rows: 1fr; }
.ema__detailin { overflow: hidden; }
03비밀번호 재설정 이메일
어떤 움직임인가
링크 만료까지 남은 시간이 10분에서 0으로 실제로 줄어드는 카운트다운이 들어 있습니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">🔒</span></td></tr>
<tr><td align="center" class="ema__title">비밀번호를 재설정하세요</td></tr>
<tr><td align="center" class="ema__body">아래 버튼을 누르면 새 비밀번호를 설정할 수 있어요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">비밀번호 재설정</a></td>
</tr></table>
</td></tr>
<tr><td align="center" class="ema__countdown">링크 만료까지 <span id="cd">10:00</span></td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-orange);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
// 다크 카드 위 추가 줄 — 밝은 모드의 rgba($subject-ink,.72) 가 그대로 남으면 잉크 면 위 잉크 글자(1.00:1)라 안 보인다 (2026-09-13 대비 스캔)
.ema.is-dark .ema__countdown { color: rgba(255,247,230,.72); }
.ema.is-dark .ema__countdown span { color: $subject-cream; }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
.ema__countdown { color: rgba(255,247,230,.72); }
.ema__countdown span { color: $subject-cream; }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge { animation: ema-pulse 2s ease-in-out infinite; }
@keyframes ema-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(47,109,246,.35); } 50% { box-shadow: 0 0 0 10px rgba(47,109,246,0); } }
.ema__countdown { font-weight: 600; font-size: 12px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: 0 !important; }
.ema__countdown span { font-family: "Space Mono", monospace; color: $subject-ink; font-weight: 700; }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}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;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema.is-dark .ema__countdown {
color: rgba(255, 247, 230, 0.72);
}
.ema.is-dark .ema__countdown span {
color: #fff7e6;
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema__countdown {
color: rgba(255, 247, 230, 0.72);
}
.ema__countdown span {
color: #fff7e6;
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge {
animation: ema-pulse 2s ease-in-out infinite;
}
@keyframes ema-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.35);
}
50% {
box-shadow: 0 0 0 10px rgba(47, 109, 246, 0);
}
}
.ema__countdown {
font-weight: 600;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 0 !important;
}
.ema__countdown span {
font-family: "Space Mono", monospace;
color: #17141a;
font-weight: 700;
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
var cd = document.getElementById('cd');
var left = 600;
setInterval(function () {
if (left <= 0) return;
left -= 1;
var m = Math.floor(left / 60), s = left % 60;
cd.textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
}, 1000);import { useState, useEffect, useRef } from "react";
import styles from "./PasswordReset.module.scss";
export type PasswordResetProps = { duration?: number; easing?: string; color?: string };
export default function PasswordReset({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: PasswordResetProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [left, setLeft] = useState(600);
useEffect(() => { const t = setInterval(() => setLeft((v) => (v > 0 ? v - 1 : v)), 1000); return () => clearInterval(t); }, []);
const mm = String(Math.floor(left / 60)).padStart(2, "0");
const ss = String(left % 60).padStart(2, "0");
return (
<main className={styles.stage} style={{ background: "#ff4d1f" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#2f6df6", animationName: "pulse", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} aria-hidden="true">🔒</span>
</div>
<div className={styles.title}>비밀번호를 재설정하세요</div>
<div className={styles.body}>아래 버튼을 누르면 새 비밀번호를 설정할 수 있어요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); }}>{"비밀번호 재설정"}</a>
</div>
<div className={styles.row} style={{ textAlign: "center" }}>만료까지 <b>{mm}:{ss}</b></div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}03 · 비밀번호 재설정 이메일 — 측정 누적면적 0.825% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
어디를 바꾸면 되나
1초마다 setInterval로 남은 초를 깎아 mm:ss 형식으로 다시 그립니다.
var left = 600;
setInterval(function () {
if (left <= 0) return;
left -= 1;
var m = Math.floor(left / 60), s = left % 60;
cd.textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
}, 1000);
04알림 이메일
무엇이 보이나
댓글·언급처럼 가장 자주 오는 범용 알림 메일입니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">🔔</span></td></tr>
<tr><td align="center" class="ema__title">새 댓글이 달렸어요</td></tr>
<tr><td align="center" class="ema__body">회원님 글에 민지님이 댓글을 남겼습니다.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">댓글 보기</a></td>
</tr></table>
</td></tr>
<tr><td class="ema__unreadrow"><span class="ema__dot" id="dot" aria-hidden="true"></span> 안읽음</td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
// 다크 카드 위 추가 줄 — 밝은 모드의 rgba($subject-ink,.72) 가 그대로 남으면 잉크 면 위 잉크 글자(1.00:1)라 안 보인다 (2026-09-13 대비 스캔)
.ema.is-dark .ema__unreadrow { color: rgba(255,247,230,.72); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
.ema__unreadrow { color: rgba(255,247,230,.72); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge { animation: ema-shake 2s ease-in-out infinite; transform-origin: top center; } // 규칙8: 배지 흔들림 위 중심 피벗
@keyframes ema-shake { 0%, 15%, 100% { transform: rotate(0); } 20% { transform: rotate(-12deg); } 25% { transform: rotate(10deg); } 30% { transform: rotate(-8deg); } 35% { transform: rotate(5deg); } 40% { transform: rotate(0); } }
.ema__unreadrow { font-weight: 600; font-size: 11px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: 0 !important; display: flex; align-items: center; gap: $sp-1; justify-content: center; }
.ema__dot { width: 7px; height: 7px; border-radius: 50%; background: $error; display: inline-block; }
.ema.is-read .ema__dot { display: none; }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: 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;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema.is-dark .ema__unreadrow {
color: rgba(255, 247, 230, 0.72);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema__unreadrow {
color: rgba(255, 247, 230, 0.72);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge {
animation: ema-shake 2s ease-in-out infinite;
transform-origin: top center;
}
@keyframes ema-shake {
0%, 15%, 100% {
transform: rotate(0);
}
20% {
transform: rotate(-12deg);
}
25% {
transform: rotate(10deg);
}
30% {
transform: rotate(-8deg);
}
35% {
transform: rotate(5deg);
}
40% {
transform: rotate(0);
}
}
.ema__unreadrow {
font-weight: 600;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 0 !important;
display: flex;
align-items: center;
gap: 4px;
justify-content: center;
}
.ema__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #c2361a;
display: inline-block;
}
.ema.is-read .ema__dot {
display: none;
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
cta.addEventListener('click', function (e) {
e.preventDefault();
ema.classList.remove('is-demo');
ema.classList.add('is-read');
cta.textContent = '읽음';
});import { useState, useEffect, useRef } from "react";
import styles from "./Notification.module.scss";
export type NotificationProps = { duration?: number; easing?: string; color?: string };
export default function Notification({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: NotificationProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [read, setRead] = useState(false);
return (
<main className={styles.stage} style={{ background: "#ffffff" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#2f6df6", animationName: "shake", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} aria-hidden="true">🔔</span>
</div>
<div className={styles.title}>새 댓글이 달렸어요</div>
<div className={styles.body}>회원님 글에 민지님이 댓글을 남겼습니다.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); setRead(true); }}>{read ? "읽음" : "댓글 보기"}</a>
</div>
<div className={styles.row} style={{ textAlign: "center", display: read ? "none" : "flex", justifyContent: "center", gap: 4 }}><span style={{ width: 7, height: 7, borderRadius: "50%", background: "#c2361a", display: "inline-block" }} /> 안읽음</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}04 · 알림 이메일 — 측정 누적면적 0.604% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
"댓글 보기"를 누르면 안읽음 점이 실제로 사라지고 버튼 라벨이 "읽음"으로 바뀝니다.
cta.addEventListener('click', function (e) {
e.preventDefault();
ema.classList.remove('is-demo');
ema.classList.add('is-read');
cta.textContent = '읽음';
});
05배송 안내 이메일
화면에서 일어나는 일
진행바가 2초 주기로 자동으로 채워지고, "배송 조회"를 누르면 배송 3단계(준비·발송·완료)가 실제로 펼쳐집니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">📦</span></td></tr>
<tr><td align="center" class="ema__title">상품이 발송됐어요</td></tr>
<tr><td align="center" class="ema__body">주문하신 상품이 출발했어요.<br>아래에서 배송 현황을 확인하세요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">배송 조회</a></td>
</tr></table>
</td></tr>
<tr><td class="ema__steprow"><div class="ema__track" aria-hidden="true"><span class="ema__fill" id="fill"></span></div><button type="button" class="ema__detailtoggle" id="detailtoggle" aria-expanded="false" aria-controls="detail">배송 단계 보기</button><div class="ema__detail" id="detail"><div class="ema__detailin"><div class="ema__li"><span>상품 준비</span><span>완료</span></div><div class="ema__li"><span>발송</span><span>진행중</span></div><div class="ema__li"><span>배송 완료</span><span>대기</span></div></div></div></td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
// 다크 카드 위 추가 줄 — 밝은 모드의 rgba($subject-ink,.72) 가 그대로 남으면 잉크 면 위 잉크 글자(1.00:1)라 안 보인다 (2026-09-13 대비 스캔)
.ema.is-dark .ema__detailtoggle, .ema.is-dark .ema__li { color: rgba(255,247,230,.72); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
.ema__detailtoggle, .ema__li { color: rgba(255,247,230,.72); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema__steprow { padding-top: 0 !important; }
.ema__track { height: 6px; border-radius: $r-pill; background: rgba(0,0,0,.08); overflow: hidden; margin-bottom: $sp-2; }
.ema__fill { display: block; height: 100%; width: 32%; border-radius: $r-pill; background: $subject-blue; transform-origin: left; } // 규칙8: 배송 단계 막대 좌측 채움
.ema.is-demo .ema__fill { animation: ema-progress 2s ease-in-out infinite; }
@keyframes ema-progress { 0%, 15% { transform: scaleX(.18); } 55%, 70% { transform: scaleX(.86); } 100% { transform: scaleX(.18); } }
.ema__detailtoggle {
width: 100%; text-align: left; background: none; border: none; cursor: pointer;
font-weight: 600; font-size: 12px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding: $sp-1 0;
}
.ema__detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows $dur-base $ease-spring; }
.ema__detailin { overflow: hidden; }
.ema.is-open .ema__detail { grid-template-rows: 1fr; }
.ema__li { display: flex; justify-content: space-between; font-weight: 500; font-size: 11px; line-height: 1.6;
font-family: inherit; color: rgba($subject-ink, .72); padding: $sp-1 0; border-top: 1px solid rgba(0,0,0,.06); }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}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);
}
.stage {
position: relative;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema.is-dark .ema__detailtoggle, .ema.is-dark .ema__li {
color: rgba(255, 247, 230, 0.72);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema__detailtoggle, .ema__li {
color: rgba(255, 247, 230, 0.72);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema__steprow {
padding-top: 0 !important;
}
.ema__track {
height: 6px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.08);
overflow: hidden;
margin-bottom: 8px;
}
.ema__fill {
display: block;
height: 100%;
width: 32%;
border-radius: 999px;
background: #2f6df6;
transform-origin: left;
}
.ema.is-demo .ema__fill {
animation: ema-progress 2s ease-in-out infinite;
}
@keyframes ema-progress {
0%, 15% {
transform: scaleX(0.18);
}
55%, 70% {
transform: scaleX(0.86);
}
100% {
transform: scaleX(0.18);
}
}
.ema__detailtoggle {
width: 100%;
text-align: left;
background: none;
border: none;
cursor: pointer;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding: 4px 0;
}
.ema__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ema__detailin {
overflow: hidden;
}
.ema.is-open .ema__detail {
grid-template-rows: 1fr;
}
.ema__li {
display: flex;
justify-content: space-between;
font-weight: 500;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding: 4px 0;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
var dt = document.getElementById('detailtoggle');
dt.addEventListener('click', function () {
ema.classList.remove('is-demo');
var open = ema.classList.toggle('is-open');
dt.setAttribute('aria-expanded', String(open));
});import { useState, useEffect, useRef } from "react";
import styles from "./Shipping.module.scss";
export type ShippingProps = { duration?: number; easing?: string; color?: string };
export default function Shipping({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: ShippingProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
return (
<main className={styles.stage} style={{ background: "#17141a" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={styles.badge} style={{ background: "#2f6df6" }} aria-hidden="true">📦</span>
</div>
<div className={styles.title}>상품이 발송됐어요</div>
<div className={styles.body}>주문하신 상품이 출발했어요.<br/>아래에서 배송 현황을 확인하세요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); }}>{"배송 조회"}</a>
</div>
<div className={styles.row}>
<div style={{ height: 6, borderRadius: 999, background: "rgba(0,0,0,.08)", overflow: "hidden", marginBottom: 8 }}>
<span className={styles.anim} style={{ display: "block", height: "100%", width: "32%", borderRadius: 999, background: color, transformOrigin: "left", animationName: "progress", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} />
</div>
<button type="button" className={styles.detailtoggle} aria-expanded={open} onClick={() => setOpen((v) => !v)}>배송 단계 보기</button>
<div className={`${styles.detail} ${open ? styles.open : ""}`}><div className={styles.detailin}>
<div className={styles.li}><span>상품 준비</span><span>완료</span></div>
<div className={styles.li}><span>발송</span><span>진행중</span></div>
<div className={styles.li}><span>배송 완료</span><span>대기</span></div>
</div></div>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}05 · 배송 안내 이메일 — 측정 누적면적 0.343% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드 읽는 법
진행바는 width가 아니라 transform: scaleX()로 채워 레이아웃을 다시 계산하지 않습니다.
.ema__fill { transform-origin: left; }
.ema.is-demo .ema__fill { animation: ema-progress 2s ease-in-out infinite; }
@keyframes ema-progress {
0%, 15% { transform: scaleX(.18); }
55%, 70% { transform: scaleX(.86); }
100% { transform: scaleX(.18); }
}
06이메일 인증 코드 메일
어떤 움직임인가
인증 코드 글자 간격이 숨쉬듯 벌어졌다 좁아지고, "복사" 버튼은 실제로 클립보드에 코드를 담습니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">✉️</span></td></tr>
<tr><td align="center" class="ema__title">이메일 주소를 인증하세요</td></tr>
<tr><td align="center" class="ema__body">아래 인증 코드를 입력하면 가입이 완료돼요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">482 913</a></td>
</tr></table>
</td></tr>
<tr><td align="center" class="ema__copyrow"><button type="button" class="ema__copybtn" id="copybtn">복사 <span id="copystate" aria-live="polite"></span></button></td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-yellow);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
// 다크 카드 위 추가 줄 — 밝은 모드의 rgba($subject-ink,.72) 가 그대로 남으면 잉크 면 위 잉크 글자(1.00:1)라 안 보인다 (2026-09-13 대비 스캔)
.ema.is-dark .ema__copybtn { color: rgba(255,247,230,.72); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
.ema__copybtn { color: rgba(255,247,230,.72); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema__copyrow { padding-top: 0 !important; }
.ema__copybtn { background: none; border: 1px solid rgba(0,0,0,.16); border-radius: $r-control; padding: $sp-1 $sp-3; font-weight: 600; font-size: 11px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); cursor: pointer; }
.ema.is-demo .ema__cta { animation: ema-stagger 2s ease-in-out infinite; }
@keyframes ema-stagger { 0%, 15% { letter-spacing: 0; } 50%, 65% { letter-spacing: 3px; } 100% { letter-spacing: 0; } }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema.is-dark .ema__copybtn {
color: rgba(255, 247, 230, 0.72);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema__copybtn {
color: rgba(255, 247, 230, 0.72);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema__copyrow {
padding-top: 0 !important;
}
.ema__copybtn {
background: none;
border: 1px solid rgba(0, 0, 0, 0.16);
border-radius: 12px;
padding: 4px 12px;
font-weight: 600;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
cursor: pointer;
}
.ema.is-demo .ema__cta {
animation: ema-stagger 2s ease-in-out infinite;
}
@keyframes ema-stagger {
0%, 15% {
letter-spacing: 0;
}
50%, 65% {
letter-spacing: 3px;
}
100% {
letter-spacing: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
var copybtn = document.getElementById('copybtn');
var copystate = document.getElementById('copystate');
copybtn.addEventListener('click', function () {
var code = cta.textContent.trim();
var ok = false;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code);
ok = true;
} else {
// 클립보드 API 가 없는 환경(오래된 웹뷰 등) 폴백 — 숨김 textarea + execCommand 로 실제 복사
var ta = document.createElement('textarea');
ta.value = code; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
}
copystate.textContent = ok ? '복사됨' : '복사 실패 — 직접 선택하세요';
setTimeout(function () { copystate.textContent = ''; }, 1500);
});import { useState, useEffect, useRef } from "react";
import styles from "./EmailVerification.module.scss";
export type EmailVerificationProps = { duration?: number; easing?: string; color?: string };
export default function EmailVerification({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: EmailVerificationProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [copyState, setCopyState] = useState("");
const code = "482 913";
function copyCode() {
let ok = false;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code); ok = true;
} else {
const ta = document.createElement("textarea");
ta.value = code; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
document.body.removeChild(ta);
}
setCopyState(ok ? "복사됨" : "복사 실패 — 직접 선택하세요");
setTimeout(() => setCopyState(""), 1500);
}
return (
<main className={styles.stage} style={{ background: "#ffd23f" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={styles.badge} style={{ background: "#2f6df6" }} aria-hidden="true">✉️</span>
</div>
<div className={styles.title}>이메일 주소를 인증하세요</div>
<div className={styles.body}>아래 인증 코드를 입력하면 가입이 완료돼요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6", animationName: "stagger", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); }}>{"482 913"}</a>
</div>
<div className={styles.row} style={{ textAlign: "center" }}>
<button type="button" className={styles.detailtoggle} style={{ display: "inline-block", width: "auto", border: "1px solid rgba(0,0,0,.16)", borderRadius: 12, padding: "4px 12px" }}
onClick={copyCode}>
복사 {copyState}
</button>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}06 · 이메일 인증 코드 메일 — 측정 누적면적 0.803% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
어디를 바꾸면 되나
navigator.clipboard가 없는 환경(오래된 웹뷰 등)에서는 숨김 <textarea>에 코드를 넣고 선택한 뒤 document.execCommand('copy')로 진짜 복사를 시도하고, 그마저 실패하면 "복사됨" 대신 "복사 실패"를 정직하게 보여 줍니다.
function copyCode(code) {
var ok = false;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code); ok = true;
} else {
var ta = document.createElement('textarea');
ta.value = code; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
}
copystate.textContent = ok ? '복사됨' : '복사 실패 — 직접 선택하세요';
}
07청구서 · 구독 결제 이메일
무엇이 보이나
0원에서 시작한 청구 금액이 requestAnimationFrame으로 실제로 12,900원까지 올라갑니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">💳</span></td></tr>
<tr><td align="center" class="ema__title">이번 달 청구서가 도착했어요</td></tr>
<tr><td align="center" class="ema__body">정기결제 금액이 이번 달 청구서에 포함되었어요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">청구서 확인</a></td>
</tr></table>
</td></tr>
<tr><td align="center" class="ema__amount">월 정기결제 <b id="amt">0원</b></td></tr><tr><td class="ema__detailrow"><button type="button" class="ema__detailtoggle" id="detailtoggle" aria-expanded="false" aria-controls="detail">청구 내역 보기</button><div class="ema__detail" id="detail"><div class="ema__detailin"><div class="ema__li"><span>Acme Pro 월간</span><span>12,900원</span></div></div></div></td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-orange);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
// 다크 카드 위 추가 줄 — 밝은 모드의 rgba($subject-ink,.72) 가 그대로 남으면 잉크 면 위 잉크 글자(1.00:1)라 안 보인다 (2026-09-13 대비 스캔)
.ema.is-dark .ema__amount, .ema.is-dark .ema__detailtoggle, .ema.is-dark .ema__li { color: rgba(255,247,230,.72); }
.ema.is-dark .ema__amount b { color: $subject-cream; }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
.ema__amount, .ema__detailtoggle, .ema__li { color: rgba(255,247,230,.72); }
.ema__amount b { color: $subject-cream; }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge { animation: ema-counterpulse 2s ease-in-out infinite; }
@keyframes ema-counterpulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }
.ema__amount { font-weight: 600; font-size: 12px; line-height: 1.5;
font-family: inherit; color: rgba($subject-ink, .72); }
.ema__amount b { font-family: "Space Mono", monospace; color: $subject-ink; font-size: 14px; }
.ema__detailrow { padding-top: 0 !important; }
.ema__detailtoggle { width: 100%; text-align: left; background: none; border: none; cursor: pointer; font-weight: 600; font-size: 12px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding: $sp-1 0; }
.ema__detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows $dur-base $ease-spring; }
.ema__detailin { overflow: hidden; }
.ema.is-open .ema__detail { grid-template-rows: 1fr; }
.ema__li { display: flex; justify-content: space-between; font-weight: 500; font-size: 11px; line-height: 1.6;
font-family: inherit; color: rgba($subject-ink, .72); padding: $sp-1 0; border-top: 1px solid rgba(0,0,0,.06); }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}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;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema.is-dark .ema__amount, .ema.is-dark .ema__detailtoggle, .ema.is-dark .ema__li {
color: rgba(255, 247, 230, 0.72);
}
.ema.is-dark .ema__amount b {
color: #fff7e6;
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
.ema__amount, .ema__detailtoggle, .ema__li {
color: rgba(255, 247, 230, 0.72);
}
.ema__amount b {
color: #fff7e6;
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge {
animation: ema-counterpulse 2s ease-in-out infinite;
}
@keyframes ema-counterpulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
}
.ema__amount {
font-weight: 600;
font-size: 12px;
line-height: 1.5;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.ema__amount b {
font-family: "Space Mono", monospace;
color: #17141a;
font-size: 14px;
}
.ema__detailrow {
padding-top: 0 !important;
}
.ema__detailtoggle {
width: 100%;
text-align: left;
background: none;
border: none;
cursor: pointer;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding: 4px 0;
}
.ema__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ema__detailin {
overflow: hidden;
}
.ema.is-open .ema__detail {
grid-template-rows: 1fr;
}
.ema__li {
display: flex;
justify-content: space-between;
font-weight: 500;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding: 4px 0;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
var amt = document.getElementById('amt');
var target = 12900, cur = 0;
var t0 = null;
function step(ts) {
if (!t0) t0 = ts;
var p = Math.min(1, (ts - t0) / 1200);
cur = Math.round(target * p);
amt.textContent = cur.toLocaleString('ko-KR') + '원';
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
var dt = document.getElementById('detailtoggle');
dt.addEventListener('click', function () {
ema.classList.remove('is-demo');
var open = ema.classList.toggle('is-open');
dt.setAttribute('aria-expanded', String(open));
});import { useState, useEffect, useRef } from "react";
import styles from "./Invoice.module.scss";
export type InvoiceProps = { duration?: number; easing?: string; color?: string };
export default function Invoice({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: InvoiceProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [amt, setAmt] = useState(0);
const raf = useRef<number | null>(null);
useEffect(() => {
const t0 = performance.now();
const tick = (ts: number) => { const p = Math.min(1, (ts - t0) / 1200); setAmt(Math.round(12900 * p)); if (p < 1) raf.current = requestAnimationFrame(tick); };
raf.current = requestAnimationFrame(tick);
return () => { if (raf.current) cancelAnimationFrame(raf.current); };
}, []);
return (
<main className={styles.stage} style={{ background: "#ff4d1f" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#2f6df6", animationName: "counterpulse", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} aria-hidden="true">💳</span>
</div>
<div className={styles.title}>이번 달 청구서가 도착했어요</div>
<div className={styles.body}>정기결제 금액이 이번 달 청구서에 포함되었어요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); }}>{"청구서 확인"}</a>
</div>
<div className={styles.row} style={{ textAlign: "center" }}>월 정기결제 <b>{amt.toLocaleString("ko-KR")}원</b></div>
<div className={styles.row}>
<button type="button" className={styles.detailtoggle} aria-expanded={open} onClick={() => setOpen((v) => !v)}>청구 내역 보기</button>
<div className={`${styles.detail} ${open ? styles.open : ""}`}><div className={styles.detailin}>
<div className={styles.li}><span>Acme Pro 월간</span><span>12,900원</span></div>
</div></div>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}07 · 청구서 · 구독 결제 이메일 — 측정 누적면적 1.118% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드에서 볼 곳
버튼을 누르면 청구 내역이 펼쳐집니다.
var target = 12900, t0 = null;
function step(ts) {
if (!t0) t0 = ts;
var p = Math.min(1, (ts - t0) / 1200);
amt.textContent = Math.round(target * p).toLocaleString('ko-KR') + '원';
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
08예약 확인 이메일
화면에서 일어나는 일
날짜 배지가 2초마다 rotateY로 뒤집히고, "캘린더에 추가"를 누르면 실제로 라벨이 "추가됨 ✓"으로 바뀝니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">📅</span></td></tr>
<tr><td align="center" class="ema__title">예약이 확정됐습니다</td></tr>
<tr><td align="center" class="ema__body">9월 15일 오후 2시, 강남점 예약이 확정됐어요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">캘린더에 추가</a></td>
</tr></table>
</td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $subject-blue; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $subject-blue; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema__badge { transform-style: preserve-3d; }
.ema.is-demo .ema__badge { animation: ema-flip 2s ease-in-out infinite; }
@keyframes ema-flip { 0%, 20% { transform: rotateY(0); } 50%, 65% { transform: rotateY(180deg); } 100% { transform: rotateY(360deg); } }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: 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;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #2f6df6;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #2f6df6;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema__badge {
transform-style: preserve-3d;
}
.ema.is-demo .ema__badge {
animation: ema-flip 2s ease-in-out infinite;
}
@keyframes ema-flip {
0%, 20% {
transform: rotateY(0);
}
50%, 65% {
transform: rotateY(180deg);
}
100% {
transform: rotateY(360deg);
}
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
cta.addEventListener('click', function (e) {
e.preventDefault();
ema.classList.remove('is-demo');
var on = ema.classList.toggle('is-done');
cta.setAttribute('aria-pressed', String(on));
cta.textContent = on ? '추가됨 ✓' : '캘린더에 추가';
});import { useState, useEffect, useRef } from "react";
import styles from "./AppointmentConfirmation.module.scss";
export type AppointmentConfirmationProps = { duration?: number; easing?: string; color?: string };
export default function AppointmentConfirmation({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: AppointmentConfirmationProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [done, setDone] = useState(false);
return (
<main className={styles.stage} style={{ background: "#ffffff" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#2f6df6", animationName: "flip", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} aria-hidden="true">📅</span>
</div>
<div className={styles.title}>예약이 확정됐습니다</div>
<div className={styles.body}>9월 15일 오후 2시, 강남점 예약이 확정됐어요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#2f6df6" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); setDone((v) => !v); }}>{done ? "추가됨 ✓" : "캘린더에 추가"}</a>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}08 · 예약 확인 이메일 — 측정 누적면적 0.619% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
코드 읽는 법
.ema__badge { transform-style: preserve-3d; }
.ema.is-demo .ema__badge { animation: ema-flip 2s ease-in-out infinite; }
@keyframes ema-flip {
0%, 20% { transform: rotateY(0); }
50%, 65% { transform: rotateY(180deg); }
100% { transform: rotateY(360deg); }
}
09보안 알림 이메일
어떤 움직임인가
새 로그인 배지가 경고색($error 토큰)으로 은은하게 번지고, 버튼을 누르면 실제로 "보호 완료됨 ✓"으로 바뀝니다.

<main class="stage">
<div class="ema is-demo" id="ema">
<!--[if mso]>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td>
<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__table">
<tr><td align="center" class="ema__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="ema__card" style="max-width:400px">
<tr><td class="ema__brand">◆ Acme</td></tr>
<tr><td align="center" class="ema__hero"><span class="ema__badge" aria-hidden="true">⚠️</span></td></tr>
<tr><td align="center" class="ema__title">새 기기에서 로그인했어요</td></tr>
<tr><td align="center" class="ema__body">iPhone · 서울에서 새 로그인이 감지됐어요.<br>본인이 아니라면 계정을 보호하세요.</td></tr>
<tr><td align="center" class="ema__ctawrap">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td class="ema__ctabtn"><a href="#" role="button" class="ema__cta" id="cta">계정 보호하기</a></td>
</tr></table>
</td></tr>
<tr><td align="center" class="ema__footer">Acme Inc · 서울 · <a href="#" class="ema__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
<!--[if mso]>
</td></tr></table>
<![endif]-->
</div>
<button type="button" class="ema-darktoggle" id="darktoggle" aria-pressed="false">다크 모드 미리보기</button>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.stage { position: relative; }
.ema { width: min(100%, 400px); }
.ema__table { border-collapse: collapse; }
.ema__wrap { padding: 0; }
.ema__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.ema__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; }
.ema__brand { font-weight: 800; font-size: 11px; line-height: 1;
font-family: inherit; color: rgba($subject-ink, .72); letter-spacing: .02em; padding-top: $sp-2 !important; }
.ema__hero { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__badge {
display: inline-flex; align-items: center; justify-content: center;
width: clamp(26px, 8vw, 34px); height: clamp(26px, 8vw, 34px);
border-radius: 50%; background: $error; font-size: clamp(13px, 4vw, 16px); line-height: 1;
}
.ema__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25;
font-family: inherit; color: $subject-ink; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__body { font-weight: 500; font-size: clamp(10px, 3vw, 11px); line-height: 1.4;
font-family: inherit; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.ema__ctawrap { padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema__ctabtn { border-radius: $r-control; background: $error; }
.ema__cta {
display: inline-block; padding: $sp-1 $sp-6; font-weight: 700; font-size: 11px; line-height: 1.6;
font-family: inherit;
color: $stage-paper; text-decoration: none; border-radius: $r-control;
}
.ema__footer { font-weight: 500; font-size: 8px; line-height: 1.4;
font-family: inherit; color: rgba($subject-ink, .72); padding-top: $sp-2 !important; padding-bottom: $sp-2 !important; }
.ema__footer a { color: inherit; }
// ── 다크모드 미리보기(수동 토글) + prefers-color-scheme(자동) — 실제 이메일 클라이언트 지원 흉내
.ema.is-dark .ema__card { background: $stage-ink; }
.ema.is-dark .ema__title { color: $subject-cream; }
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer { color: rgba(255,247,230,.6); }
.ema.is-dark .ema__brand { color: rgba(255,247,230,.7); }
@media (prefers-color-scheme: dark) {
.ema__card { background: $stage-ink; }
.ema__title { color: $subject-cream; }
.ema__body, .ema__footer { color: rgba(255,247,230,.6); }
.ema__brand { color: rgba(255,247,230,.7); }
}
// 레이아웃 흐름에서 빼 카드 높이에 영향을 주지 않는다 (480×300 무대에 맞추려고)
.ema-darktoggle {
position: absolute; top: $sp-2; right: $sp-2;
background: $stage-ink; border: 1px solid rgba(255,247,230,.3);
color: $subject-cream; border-radius: $r-pill; padding: $sp-1 $sp-3; font-weight: 600; font-size: 9px; line-height: 1.4;
font-family: inherit; cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge { animation: ema-glow 2s ease-in-out infinite; }
@keyframes ema-glow { 0%, 100% { box-shadow: 0 0 0 0 rgba(194,54,26,.4); } 50% { box-shadow: 0 0 0 12px rgba(194,54,26,0); } }
// 320×200 폰 실측(BD9 규칙)에서 6개 항목(추가 줄이 있는 02·03·04·05·06·07)이 10~46px 넘쳐
// 행마다 세로 패딩을 !important 로 다시 줄인다 (가로 폭만 줄이던 첫 시도로는 안 됐다 — 실측 2026-09-11)
@media (max-width: 340px), (max-height: 220px) {
.ema__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.ema__brand { padding-top: 0 !important; font-size: 9px !important; }
.ema__hero { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__badge { width: 22px; height: 22px; font-size: 11px; }
.ema__title { font-size: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__body { font-size: 8px !important; padding-bottom: 0 !important; }
.ema__ctawrap { padding-top: 0 !important; padding-bottom: 0 !important; }
.ema__cta { padding: 0 $sp-5 !important; }
.ema__footer { font-size: 7px !important; padding-top: $sp-1 !important; padding-bottom: $sp-1 !important; }
.ema-darktoggle { font-size: 8px; padding: 0 $sp-2; top: $sp-1; right: $sp-1; }
// 항목별 "extra" 줄(펼침 버튼·카운트다운·안읽음·복사·금액) 공통 압축 — 해당 클래스가 없는 항목엔 영향 없음
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important; padding-bottom: 0 !important; font-size: 9px !important;
}
.ema__li { padding: 0 $sp-3 !important; font-size: 9px !important; }
.ema__track { margin-bottom: 0 !important; }
.ema__copybtn { padding: 0 $sp-2 !important; }
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after { animation: none !important; }
.ema__detail, .ema__card, .ema__cta { transition: none; }
}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);
}
.stage {
position: relative;
}
.ema {
width: min(100%, 400px);
}
.ema__table {
border-collapse: collapse;
}
.ema__wrap {
padding: 0;
}
.ema__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.ema__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.ema__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.ema__hero {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(26px, 8vw, 34px);
height: clamp(26px, 8vw, 34px);
border-radius: 50%;
background: #c2361a;
font-size: clamp(13px, 4vw, 16px);
line-height: 1;
}
.ema__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
font-family: inherit;
color: #17141a;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__body {
font-weight: 500;
font-size: clamp(10px, 3vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.ema__ctawrap {
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema__ctabtn {
border-radius: 12px;
background: #c2361a;
}
.ema__cta {
display: inline-block;
padding: 4px 24px;
font-weight: 700;
font-size: 11px;
line-height: 1.6;
font-family: inherit;
color: #ffffff;
text-decoration: none;
border-radius: 12px;
}
.ema__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.ema__footer a {
color: inherit;
}
.ema.is-dark .ema__card {
background: #17141a;
}
.ema.is-dark .ema__title {
color: #fff7e6;
}
.ema.is-dark .ema__body,
.ema.is-dark .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema.is-dark .ema__brand {
color: rgba(255, 247, 230, 0.7);
}
@media (prefers-color-scheme: dark) {
.ema__card {
background: #17141a;
}
.ema__title {
color: #fff7e6;
}
.ema__body, .ema__footer {
color: rgba(255, 247, 230, 0.6);
}
.ema__brand {
color: rgba(255, 247, 230, 0.7);
}
}
.ema-darktoggle {
position: absolute;
top: 8px;
right: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.3);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 600;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
font-family: inherit;
}
.ema.is-demo .ema__badge {
animation: ema-glow 2s ease-in-out infinite;
}
@keyframes ema-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(194, 54, 26, 0.4);
}
50% {
box-shadow: 0 0 0 12px rgba(194, 54, 26, 0);
}
}
@media (max-width: 340px), (max-height: 220px) {
.ema__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.ema__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.ema__hero {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__badge {
width: 22px;
height: 22px;
font-size: 11px;
}
.ema__title {
font-size: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.ema__ctawrap {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ema__cta {
padding: 0 20px !important;
}
.ema__footer {
font-size: 7px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.ema-darktoggle {
font-size: 8px;
padding: 0 8px;
top: 4px;
right: 4px;
}
.ema__detailtoggle, .ema__countdown, .ema__unreadrow, .ema__amount, .ema__copyrow, .ema__steprow {
padding-top: 0 !important;
padding-bottom: 0 !important;
font-size: 9px !important;
}
.ema__li {
padding: 0 12px !important;
font-size: 9px !important;
}
.ema__track {
margin-bottom: 0 !important;
}
.ema__copybtn {
padding: 0 8px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.ema.is-demo .ema__badge,
.ema.is-demo .ema__fill,
.ema.is-demo .ema__badge::after {
animation: none !important;
}
.ema__detail, .ema__card, .ema__cta {
transition: none;
}
}var ema = document.getElementById('ema');
var cta = document.getElementById('cta');
var darktoggle = document.getElementById('darktoggle');
darktoggle.addEventListener('click', function () {
var on = ema.classList.toggle('is-dark');
darktoggle.setAttribute('aria-pressed', String(on));
});
cta.addEventListener('click', function (e) {
e.preventDefault();
ema.classList.remove('is-demo');
var on = ema.classList.toggle('is-done');
cta.setAttribute('aria-pressed', String(on));
cta.textContent = on ? '보호 완료됨 ✓' : '계정 보호하기';
});import { useState, useEffect, useRef } from "react";
import styles from "./SecurityAlert.module.scss";
export type SecurityAlertProps = { duration?: number; easing?: string; color?: string };
export default function SecurityAlert({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: SecurityAlertProps) {
const [dark, setDark] = useState(false);
const [open, setOpen] = useState(false);
const [done, setDone] = useState(false);
return (
<main className={styles.stage} style={{ background: "#17141a" }}>
<div className={`${styles.ema}`}>
<div className={`${styles.card} ${dark ? styles.dark : ""}`}>
<div className={styles.row + " " + styles.brand}>◆ Acme</div>
<div className={styles.hero}>
<span className={`${styles.badge} ${styles.anim}`} style={{ background: "#c2361a", animationName: "glow", animationDuration: "2s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite" }} aria-hidden="true">⚠️</span>
</div>
<div className={styles.title}>새 기기에서 로그인했어요</div>
<div className={styles.body}>iPhone · 서울에서 새 로그인이 감지됐어요.<br/>본인이 아니라면 계정을 보호하세요.</div>
<div className={styles.ctawrap}>
<a href="#" role="button" className={styles.cta} style={{ background: "#c2361a" }} onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); setDone((v) => !v); }}>{done ? "보호 완료됨 ✓" : "계정 보호하기"}</a>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#">수신거부</a></div>
</div>
<button type="button" className={styles.darktoggle} aria-pressed={dark} onClick={() => setDark((v) => !v)}>다크 모드 미리보기</button>
</div>
</main>
);
}09 · 보안 알림 이메일 — 측정 누적면적 0.878% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
어디를 바꾸면 되나
색은 다른 여덟 개의 브랜드 블루가 아니라 오류 색을 그대로 씁니다 — 안심시키는 알림이 아니라 경계해야 하는 알림이기 때문입니다.
.ema.is-demo .ema__badge { animation: ema-glow 2s ease-in-out infinite; }
@keyframes ema-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(194, 54, 26, .4); }
50% { box-shadow: 0 0 0 12px rgba(194, 54, 26, 0); }
}
어디서 깨지는가 — 함정
가장 크게 걸린 함정은 아웃룩(데스크톱)이 @keyframes·flexbox·많은 최신 CSS를 지원하지 않는다는 것입니다. 그래서 아홉 개 전부 레이아웃은 <table role="presentation">로 짜고, 폭은 width="100%"에 style="max-width:400px"를 같이 줘 표 폭이 고정되는 클라이언트에서도 무너지지 않게 했습니다. <!--[if mso]>~<![endif]--> 조건부 주석으로 아웃룩만 별도 표 래퍼를 씌운 것도 같은 이유입니다. 두 번째 함정은 실제로 코드에서 겪었습니다 — 제목·버튼 글자에 font: 700 12px/1 inherit;처럼 축약형을 쓰면 inherit이 font-family 자리에서 무효값이 되어 선언 전체가 죽고, 브라우저 기본 글꼴(13px 근처)로 조용히 되돌아갑니다. font-family만 다음 줄로 빼고 font: 700 12px/1;을 그대로 두어도 글꼴 이름이 빠진 단축은 여전히 무효라 살아나지 않으니, 단축을 버리고 font-weight: 700; font-size: 12px; line-height: 1;처럼 낱개 속성으로 쓰고 글꼴은 상속에 맡기거나(또는 단축 안에 실제 글꼴 이름까지 적어야) 시스템 서체가 제대로 걸립니다. 세 번째는 크기입니다 — 처음 아홉 개 모두 480×300 무대에서 스크롤바가 뜰 만큼 넘쳤고, 셀 안쪽 여백을 8px에서 4px로 줄이고 다크 모드 버튼을 카드 레이아웃 밖(절대 위치)으로 빼 480×300은 잡았지만, 320×200 폰 화면에서는 펼침 버튼·카운트다운 같은 "추가 줄"이 있는 여섯 개가 여전히 10~46px 넘쳤습니다. 그 여섯 개의 추가 줄 안쪽 요소(버튼·목록 줄·진행바 여백)까지 별도로 줄여야 320×200도 잡혔습니다. 네 번째는 02번 영수증의 반짝임 애니메이션이었습니다 — 처음엔 2초 중 0.7초만 움직여 23프레임 중 4프레임만 "움직인 프레임"으로 잡혔고(하한 6), 움직이는 구간을 2초 중 1.8초로 늘리고서야 16프레임으로 올랐습니다. 메일 본문이 아니라 웹 페이지에 사람 한 명을 세워 두는 카드는 프로필 카드 디자인 9종에서 다뤘습니다. 압축 비밀번호 4m43f23g로 열리는 zip 안 vanilla 버전은 이 네 가지를 전부 반영한 최신본입니다.
접근성
아홉 개 전부 prefers-reduced-motion: reduce에서 배지·진행바의 반복 애니메이션을 끄되(뒤집힘·펄스·글로우·진행바 채움 전부 animation: none !important), 클릭으로 바뀌는 실제 상태(읽음 처리·펼침·완료 표시)는 그대로 남깁니다. 버튼은 전부 role="button" 또는 실제 <button> 요소로 키보드 포커스와 Enter·Space 조작이 되고, 펼침 요소는 aria-expanded로, 토글형 버튼은 aria-pressed로 켜짐·꺼짐을 전합니다. 인증 코드 복사(06)의 상태 문구는 aria-live="polite"라 스크린 리더가 화면을 옮기지 않아도 "복사됨"을 읽어 줍니다. 다크 모드는 수동 토글 버튼과 함께 <meta name="color-scheme" content="light dark"> + @media (prefers-color-scheme: dark)도 같이 넣어, 시스템 다크 모드를 지원하는 클라이언트에서는 버튼 없이도 자동으로 맞춰집니다.
다른 이메일 소재는 템플릿 카테고리에 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다. 이메일 클라이언트별 CSS 지원 범위는 Can I email에서 항목별로 확인할 수 있습니다.
FAQ
이메일 템플릿 html 을 실제 이메일 서비스(ESP)에 그대로 써도 되나요?
네, 그러려고 만들었습니다. zip 안 vanilla 버전은 표 기반 레이아웃 + 인라인에 가까운 스타일 + max-width:400px 유동형 카드로 짜여 있어 Mailchimp·SendGrid·Resend 같은 서비스의 HTML 붙여넣기 칸에 그대로 넣을 수 있습니다. 400px 은 뉴스레터보다 좁은 거래형 카드에 맞춘 폭이라, 필요하면 max-width 값만 바꿔 더 넓은 레이아웃에 맞출 수 있습니다. 다만 실제 발송 전엔 항상 Can I email로 그 서비스가 쓰는 클라이언트의 지원 범위를 한 번 더 확인하십시오. 메일에 실려 가는 로그인 카드의 웹판은 로그인 화면 디자인 9가지에서 만날 수 있습니다.
왜 아홉 개 색이 다 파란색이고 09번만 빨간색인가요?
여덟 개는 브랜드 블루($subject-blue)를 CTA·배지 색으로 공유해 한 회사가 보내는 메일처럼 통일감을 주었고, 09번 보안 알림만 오류 토큰($error)을 썼습니다. 안심시켜야 하는 메일과 경계해야 하는 메일은 색부터 달라야 한다고 판단했습니다.
다크 모드 미리보기 버튼은 실제 다크 모드와 같은 동작인가요?
네, 같은 CSS 규칙을 씁니다. 버튼은 .is-dark 클래스를 토글해 @media (prefers-color-scheme: dark)와 똑같은 배경·글자색 규칙을 강제로 켜는 것이라, 버튼을 눌러 보는 화면이 실제 다크 모드 지원 클라이언트(Apple Mail·Outlook.com 등)에서 보이는 화면과 같습니다.









