06온보딩 체크리스트
다섯 칸 진행 막대가 세 칸까지 차 있고 네 번째 줄의 체크 표시가 선을 따라 그려져, 다음에 할 일 하나만 남았다는 걸 보여줍니다.
쓰임가입 직후 연속 메일, 기능 소개 시리즈
stroke-dashoffsetgrid-template-columnsaria-valuenow

<main class="stage">
<div class="mk is-demo" id="mk">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="mk__table">
<tr><td align="center" class="mk__wrap">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" class="mk__card" style="max-width:400px">
<tr><td class="mk__brand">◆ Acme</td></tr>
<tr><td align="center" class="mk__title">반가워요! 시작할 준비가 거의 끝났어요</td></tr>
<tr><td align="center" class="mk__body">5단계 중 3단계를 끝냈어요. 아래 칸을 눌러 다음 단계를 끝내 보세요.</td></tr>
<tr><td class="mk__barrow">
<div class="mk__bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="시작 단계 진행률" id="bar">
<span class="mk__seg is-fill"></span><span class="mk__seg is-fill"></span><span class="mk__seg is-fill"></span><span class="mk__seg" id="seg4"></span><span class="mk__seg"></span>
</div>
<span class="mk__barlabel"><span id="nowpct">3</span>/5</span>
</td></tr>
<tr><td class="mk__list">
<div class="mk__step2 is-done">
<span class="mk__done" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="mk__ico"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg></span>
<span class="mk__label">이메일 주소 확인</span>
</div>
<div class="mk__step2 is-done">
<span class="mk__done" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="mk__ico"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg></span>
<span class="mk__label">프로필 사진 올리기</span>
</div>
<div class="mk__step2 is-done">
<span class="mk__done" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="mk__ico"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg></span>
<span class="mk__label">첫 소식 구독하기</span>
</div>
<button type="button" class="mk__step2 mk__next" id="next" aria-pressed="false">
<span class="mk__draw" aria-hidden="true"><svg viewBox="0 0 24 24" class="mk__drawsvg"><polyline class="mk__line" pathLength="1" points="4,13 9.5,18.5 20,6" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="mk__label">알림 설정하기</span>
<span class="mk__hint">눌러서 완료</span>
</button>
<div class="mk__step2 is-todo">
<span class="mk__todo" aria-hidden="true"></span>
<span class="mk__label">친구 초대하기</span>
</div>
</td></tr>
<tr><td align="center" class="mk__footer">Acme Inc · 서울 · <a href="#" class="mk__unsub">수신거부</a></td></tr>
</table>
</td></tr>
</table>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter;
$easing: $ease-out;
$grass: #479074;
$color: $grass;
@include stage($stage-yellow);
.stage { position: relative; }
.mk { width: min(100%, 400px); }
.mk__table { border-collapse: collapse; }
.mk__wrap { padding: 0; }
.mk__card {
border-collapse: collapse; background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
}
.mk__card td { padding: $sp-1 $sp-5; font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; -webkit-text-size-adjust: 100%; line-height: 1; }
.mk__brand { font-weight: 800; font-size: 11px; line-height: 1; color: rgba(23,20,26,.62); letter-spacing: .02em; padding-top: $sp-2 !important; }
.mk__title { font-weight: 800; font-size: clamp(12px, 3.6vw, 14px); line-height: 1.25; color: $subject-ink; padding-top: $sp-1 !important; }
.mk__body { font-weight: 500; font-size: clamp(9px, 2.8vw, 10px); line-height: 1.4; color: rgba(23,20,26,.62); padding-top: 0 !important; padding-bottom: $sp-1 !important; }
.mk__barrow { display: flex; align-items: center; gap: $sp-2; padding-top: 0 !important; padding-bottom: 0 !important; }
.mk__bar {
display: grid; grid-template-columns: repeat(5, 1fr); gap: $sp-1; flex: 1;
}
.mk__seg { height: 6px; border-radius: $r-pill; background: rgba(23,20,26,.1); }
.mk__seg.is-fill { background: $grass; }
.mk__barlabel { font-weight: 800; font-size: 10px; line-height: 1; color: rgba($subject-ink, .72); font-variant-numeric: tabular-nums; white-space: nowrap; }
.mk__list { padding-top: $sp-1 !important; padding-bottom: 0 !important; }
.mk__step2 {
display: flex; align-items: center; gap: $sp-2; width: 100%; text-align: left;
background: none; border: none; cursor: default; font-family: inherit;
padding: $sp-1 0; border-top: 1px solid rgba(0,0,0,.06);
}
.mk__done, .mk__draw, .mk__todo {
display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
width: 18px; height: 18px; border-radius: 50%;
}
.mk__done { background: $grass; color: $subject-cream; }
.mk__ico { width: 12px; height: 12px; }
.mk__todo { background: rgba(23,20,26,.07); }
.mk__label { font-weight: 700; font-size: clamp(10px, 3vw, 12px); line-height: 1.2; color: rgba(23,20,26,.62); }
.is-done .mk__label { color: $subject-ink; }
.mk__next { cursor: pointer; background: rgba(76,212,166,.12); border-radius: $r-sm; padding-left: $sp-1; padding-right: $sp-1; }
.mk__next .mk__label { color: $subject-ink; }
.mk__draw { background: rgba(76,212,166,.3); color: $subject-ink; }
.mk__drawsvg { width: 12px; height: 12px; }
.mk__line { stroke-dasharray: 1; stroke-dashoffset: 1; }
.mk__next.is-done .mk__draw { background: $grass; }
.mk__next.is-done .mk__line { stroke-dashoffset: 0; }
.mk__next.is-done .mk__hint { visibility: hidden; }
.mk__hint { margin-left: auto; font-weight: 700; font-size: 8px; line-height: 1; color: $grass; background: $stage-paper; border-radius: $r-pill; padding: $sp-1 $sp-2; white-space: nowrap; }
.mk__next:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; border-radius: $r-sm; }
.mk__next:hover { background: rgba(76,212,166,.2); }
.mk__footer { font-weight: 500; font-size: 8px; line-height: 1.4; color: rgba(23,20,26,.62); padding-top: $sp-1 !important; padding-bottom: $sp-2 !important; }
.mk__footer a { color: inherit; }
// ── 미리보기 루프: 네 번째 줄 체크가 선을 따라 그려지고(0.6s), 힌트 알약이 맥박 (2s 주기, 복귀 86%)
.mk.is-demo .mk__line { animation-name: obDraw; animation-duration: 2s; animation-timing-function: $ease-out; animation-iteration-count: infinite; }
.mk.is-demo .mk__draw { animation-name: obFill; animation-duration: 2s; animation-timing-function: steps(1, end); animation-iteration-count: infinite; }
.mk.is-demo .mk__hint { animation-name: obHint; animation-duration: 2s; animation-timing-function: $ease-pop; animation-iteration-count: infinite; }
@keyframes obDraw {
0%, 8% { stroke-dashoffset: 1; }
38% { stroke-dashoffset: 0; }
84% { stroke-dashoffset: 0; }
86%, 100% { stroke-dashoffset: 1; }
}
@keyframes obFill {
0%, 8% { background: rgba(76,212,166,.3); }
38%, 84% { background: #479074; }
86%, 100% { background: rgba(76,212,166,.3); }
}
@keyframes obHint {
0%, 40% { transform: scale(1); }
50% { transform: scale(1.12); }
60%, 84% { transform: scale(1); }
86%, 100% { transform: scale(1); }
}
// ── 320×200 폰 실측 대비 압축 (BD9)
@media (max-width: 340px), (max-height: 220px) {
.mk__card td { padding-left: $sp-3; padding-right: $sp-3; padding-top: 0; padding-bottom: 0; }
.mk__brand { padding-top: 0 !important; font-size: 9px !important; }
.mk__title { font-size: 11px !important; padding-top: 0 !important; }
.mk__body { font-size: 8px !important; padding-bottom: 0 !important; }
.mk__seg { height: 4px; }
.mk__label { font-size: 9px !important; }
.mk__step2 { padding: 0 !important; }
.mk__hint { font-size: 7px !important; padding: 0 $sp-1 !important; }
.mk__footer { font-size: 7px !important; padding-top: 0 !important; padding-bottom: $sp-1 !important; }
}
@media (prefers-reduced-motion: reduce) {
.mk.is-demo .mk__line,
.mk.is-demo .mk__draw,
.mk.is-demo .mk__hint { animation: none !important; }
.mk.is-demo .mk__line { stroke-dashoffset: 0; }
.mk.is-demo .mk__draw { background: $grass; }
}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;
}
.mk {
width: min(100%, 400px);
}
.mk__table {
border-collapse: collapse;
}
.mk__wrap {
padding: 0;
}
.mk__card {
border-collapse: collapse;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.mk__card td {
padding: 4px 20px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
-webkit-text-size-adjust: 100%;
line-height: 1;
}
.mk__brand {
font-weight: 800;
font-size: 11px;
line-height: 1;
color: rgba(23, 20, 26, 0.62);
letter-spacing: 0.02em;
padding-top: 8px !important;
}
.mk__title {
font-weight: 800;
font-size: clamp(12px, 3.6vw, 14px);
line-height: 1.25;
color: #17141a;
padding-top: 4px !important;
}
.mk__body {
font-weight: 500;
font-size: clamp(9px, 2.8vw, 10px);
line-height: 1.4;
color: rgba(23, 20, 26, 0.62);
padding-top: 0 !important;
padding-bottom: 4px !important;
}
.mk__barrow {
display: flex;
align-items: center;
gap: 8px;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.mk__bar {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 4px;
flex: 1;
}
.mk__seg {
height: 6px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.1);
}
.mk__seg.is-fill {
background: #479074;
}
.mk__barlabel {
font-weight: 800;
font-size: 10px;
line-height: 1;
color: rgba(23, 20, 26, 0.72);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.mk__list {
padding-top: 4px !important;
padding-bottom: 0 !important;
}
.mk__step2 {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
background: none;
border: none;
cursor: default;
font-family: inherit;
padding: 4px 0;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.mk__done, .mk__draw, .mk__todo {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 18px;
height: 18px;
border-radius: 50%;
}
.mk__done {
background: #479074;
color: #fff7e6;
}
.mk__ico {
width: 12px;
height: 12px;
}
.mk__todo {
background: rgba(23, 20, 26, 0.07);
}
.mk__label {
font-weight: 700;
font-size: clamp(10px, 3vw, 12px);
line-height: 1.2;
color: rgba(23, 20, 26, 0.62);
}
.is-done .mk__label {
color: #17141a;
}
.mk__next {
cursor: pointer;
background: rgba(76, 212, 166, 0.12);
border-radius: 8px;
padding-left: 4px;
padding-right: 4px;
}
.mk__next .mk__label {
color: #17141a;
}
.mk__draw {
background: rgba(76, 212, 166, 0.3);
color: #17141a;
}
.mk__drawsvg {
width: 12px;
height: 12px;
}
.mk__line {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.mk__next.is-done .mk__draw {
background: #479074;
}
.mk__next.is-done .mk__line {
stroke-dashoffset: 0;
}
.mk__next.is-done .mk__hint {
visibility: hidden;
}
.mk__hint {
margin-left: auto;
font-weight: 700;
font-size: 8px;
line-height: 1;
color: #479074;
background: #ffffff;
border-radius: 999px;
padding: 4px 8px;
white-space: nowrap;
}
.mk__next:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
border-radius: 8px;
}
.mk__next:hover {
background: rgba(76, 212, 166, 0.2);
}
.mk__footer {
font-weight: 500;
font-size: 8px;
line-height: 1.4;
color: rgba(23, 20, 26, 0.62);
padding-top: 4px !important;
padding-bottom: 8px !important;
}
.mk__footer a {
color: inherit;
}
.mk.is-demo .mk__line {
animation-name: obDraw;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.mk.is-demo .mk__draw {
animation-name: obFill;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.mk.is-demo .mk__hint {
animation-name: obHint;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
@keyframes obDraw {
0%, 8% {
stroke-dashoffset: 1;
}
38% {
stroke-dashoffset: 0;
}
84% {
stroke-dashoffset: 0;
}
86%, 100% {
stroke-dashoffset: 1;
}
}
@keyframes obFill {
0%, 8% {
background: rgba(76, 212, 166, 0.3);
}
38%, 84% {
background: #479074;
}
86%, 100% {
background: rgba(76, 212, 166, 0.3);
}
}
@keyframes obHint {
0%, 40% {
transform: scale(1);
}
50% {
transform: scale(1.12);
}
60%, 84% {
transform: scale(1);
}
86%, 100% {
transform: scale(1);
}
}
@media (max-width: 340px), (max-height: 220px) {
.mk__card td {
padding-left: 12px;
padding-right: 12px;
padding-top: 0;
padding-bottom: 0;
}
.mk__brand {
padding-top: 0 !important;
font-size: 9px !important;
}
.mk__title {
font-size: 11px !important;
padding-top: 0 !important;
}
.mk__body {
font-size: 8px !important;
padding-bottom: 0 !important;
}
.mk__seg {
height: 4px;
}
.mk__label {
font-size: 9px !important;
}
.mk__step2 {
padding: 0 !important;
}
.mk__hint {
font-size: 7px !important;
padding: 0 4px !important;
}
.mk__footer {
font-size: 7px !important;
padding-top: 0 !important;
padding-bottom: 4px !important;
}
}
@media (prefers-reduced-motion: reduce) {
.mk.is-demo .mk__line,
.mk.is-demo .mk__draw,
.mk.is-demo .mk__hint {
animation: none !important;
}
.mk.is-demo .mk__line {
stroke-dashoffset: 0;
}
.mk.is-demo .mk__draw {
background: #479074;
}
}var mk = document.getElementById('mk');
var next = document.getElementById('next');
var seg4 = document.getElementById('seg4');
var bar = document.getElementById('bar');
var nowpct = document.getElementById('nowpct');
next.addEventListener('click', function () {
mk.classList.remove('is-demo');
var done = next.classList.toggle('is-done');
next.setAttribute('aria-pressed', String(done));
seg4.classList.toggle('is-fill', done);
bar.setAttribute('aria-valuenow', done ? '80' : '60');
nowpct.textContent = done ? '4' : '3';
});import { useState } from "react";
import styles from "./OnboardingChecklist.module.scss";
export type OnboardingChecklistProps = { duration?: number; easing?: string; color?: string };
const DONE = ["이메일 주소 확인", "프로필 사진 올리기", "첫 소식 구독하기"];
const NEXT = "알림 설정하기";
const TODO = "친구 초대하기";
export default function OnboardingChecklist({ duration = 600, easing = "ease-out", color = "#0f7a52" }: OnboardingChecklistProps) {
const [demo, setDemo] = useState(true);
const [done4, setDone4] = useState(false);
const steps = 3 + (done4 ? 1 : 0);
return (
<main className={styles.stage}>
<div className={styles.mk}>
<div className={styles.card}>
<div className={styles.brand}>◆ Acme</div>
<div className={styles.title}>반가워요! 시작할 준비가 거의 끝났어요</div>
<div className={styles.body}>5단계 중 3단계를 끝냈어요. 아래 칸을 눌러 다음 단계를 끝내 보세요.</div>
<div className={styles.barrow}>
<div className={styles.bar} role="progressbar" aria-valuenow={steps * 20} aria-valuemin={0} aria-valuemax={100} aria-label="시작 단계 진행률">
{[0, 1, 2, 3, 4].map((i) => (
<span key={i} className={`${styles.seg} ${(i < 3 || (i === 3 && done4)) ? styles.isFill : ""}`}></span>
))}
</div>
<span className={styles.barlabel}>{steps}/5</span>
</div>
<div className={styles.list}>
{DONE.map((label) => (
<div key={label} className={`${styles.stepRow} ${styles.isDone}`}>
<span className={styles.done} style={{ background: "#0f7a52" }} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" className={styles.ico}><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg>
</span>
<span className={styles.label}>{label}</span>
</div>
))}
<button
type="button"
className={`${styles.stepRow} ${styles.next} ${done4 ? styles.isDone : ""}`}
aria-pressed={done4}
onClick={() => { setDemo(false); setDone4((v) => !v); }}
>
<span className={styles.draw} style={{ ...(demo ? { animationName: "obFill", animationDuration: "2s", animationTimingFunction: "steps(1, end)", animationIterationCount: "infinite", background: color } : (done4 ? { background: "#0f7a52" } : {})) }} aria-hidden="true">
<svg viewBox="0 0 24 24" className={styles.drawsvg}><polyline className={styles.line} pathLength="1" points="4,13 9.5,18.5 20,6" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style={demo ? { animationName: "obDraw", animationDuration: "2s", animationTimingFunction: easing, animationIterationCount: "infinite" } : undefined} /></svg>
</span>
<span className={styles.label}>{NEXT}</span>
<span className={styles.hint} style={demo ? { animationName: "obHint", animationDuration: "2s", animationTimingFunction: easing, animationIterationCount: "infinite" } : undefined}>눌러서 완료</span>
</button>
<div className={`${styles.stepRow} ${styles.isTodo}`}>
<span className={styles.todo} aria-hidden="true"></span>
<span className={styles.label}>{TODO}</span>
</div>
</div>
<div className={styles.footer}>Acme Inc · 서울 · <a href="#" className={styles.unsub}>수신거부</a></div>
</div>
</div>
</main>
);
}06 온보딩 체크리스트 — 측정 누적면적 0.528% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 뉴스레터 디자인 9종 — 메일 앱에서 안 깨지는 HTML
이 데모가 속한 글
뉴스레터 디자인 9종 — 메일 앱에서 안 깨지는 HTML뉴스레터 디자인은 정기적으로 오는 마케팅 메일의 겉모습을 짜는 일입니다. 여기 아홉 가지는 다이제스트·쿠폰·타임세일·장바구니 리마인드 같은 자주 보내는 마케팅 메일을 HTML 과 CSS 로 짜 둔 것이라, 메일 앱에 이 글의 데모 전부 보기

