09글자를 따라 쳐야 열리는 계정 삭제
붉은 구역 안 입력칸에 정해진 낱말을 그대로 쳐 넣기 전에는 삭제가 잠겨 있고, 글자가 맞는 순간에만 버튼이 열립니다. 눌러서 뜨는 확인창은 경고를 읽어 주는 역할로 열려 초점이 먼저 그 안으로 들어갑니다.
쓰임되돌릴 수 없는 계정 삭제를 마지막으로 한 번 더 막아 두는 자리
role="alertdialog"aria-disabledoutline

<main class="stage">
<section class="dz is-demo" data-open="no">
<p class="dz__head">
<span class="dz__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256"><g fill="currentColor"><path d="M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91" opacity=".2"/><path d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg></span>
위험 구역
</p>
<p class="dz__desc">계정을 지우면 글과 파일이 함께 사라집니다. 확인 문구에 DELETE 를 그대로 입력하세요.</p>
<label class="dz__label" for="dz-input">확인 문구</label>
<span class="dz__slot">
<input class="dz__input" id="dz-input" type="text" autocomplete="off" spellcheck="false" placeholder="DELETE">
<span class="dz__ghost" aria-hidden="true"><span class="dz__typed">DELETE</span></span>
</span>
<button class="dz__btn" id="dz-del" type="button" aria-disabled="true">
<span class="dz__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256"><g fill="currentColor"><path d="M200 56v152a8 8 0 0 1-8 8H64a8 8 0 0 1-8-8V56Z" opacity=".2"/><path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16M96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0m48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0"/></g></svg></span>
계정 삭제
</button>
<p class="dz__live" id="dz-live" role="status" aria-live="polite">
<span class="dz__word" data-word="lock">DELETE 를 정확히 입력해야 열립니다</span>
<span class="dz__word" data-word="open">삭제 버튼이 열렸습니다</span>
</p>
<span class="dz__scrim" aria-hidden="true"></span>
<div class="dz__dialog" id="dz-dialog" role="alertdialog" aria-modal="true" aria-labelledby="dz-dtitle" aria-describedby="dz-dbody">
<p class="dz__dtitle" id="dz-dtitle">정말 삭제할까요?</p>
<p class="dz__dbody" id="dz-dbody">30일 안에는 되돌릴 수 있습니다.</p>
<span class="dz__drow">
<button class="dz__dbtn dz__dbtn--no" id="dz-no" type="button">취소</button>
<button class="dz__dbtn dz__dbtn--yes" id="dz-yes" type="button">삭제합니다</button>
</span>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-ink);
// 확인창은 position: absolute 로 카드 위에 띄운다. 카드 자체가 확인창보다 높아
// 뿌리가 이미 그 높이를 품고 있다 — 그래서 scrollHeight 가 무대를 넘지 않는다 (BD8·BD9)
.dz {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $sp-2;
width: min(300px, 88vw);
padding: $sp-3;
border-radius: $r-card;
background-color: $subject-cream;
color: $subject-ink;
box-shadow: $shadow-raised;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.dz__ic { display: inline-flex; flex: 0 0 auto; }
.dz__ic svg { display: block; }
.dz__head {
display: flex;
align-items: center;
gap: $sp-1;
margin: 0;
font-size: 13px;
line-height: 18px;
font-weight: 800;
color: $error;
}
.dz__desc {
margin: 0;
font-size: 11px;
line-height: 16px;
font-weight: 500;
color: $subject-ink;
}
.dz__label {
display: block;
font-size: 10px;
line-height: 14px;
font-weight: 700;
color: $error;
}
.dz__slot { position: relative; display: block; }
.dz__input {
box-sizing: border-box;
display: block;
width: 100%;
height: 32px;
margin: 0;
padding: 0 $sp-2;
border: 0;
border-radius: $r-sm;
background-color: $stage-paper;
color: $subject-ink;
font-family: ui-monospace, "Consolas", monospace;
font-size: 14px;
line-height: 32px;
font-weight: 700;
box-shadow: inset 0 0 0 2px $error-soft;
}
.dz__input:focus-visible { outline: 3px solid $color; outline-offset: 2px; }
// 자동 루프 동안만 한 자씩 찍히는 글자를 진짜 입력칸 위에 겹친다
.dz__ghost {
position: absolute;
left: $sp-2;
top: 0;
bottom: 0;
display: none;
align-items: center;
}
.dz.is-demo .dz__ghost { display: flex; }
.dz.is-demo .dz__input { color: transparent; }
.dz.is-demo .dz__input::placeholder { color: transparent; }
.dz__typed {
display: block;
overflow: hidden;
width: 0;
white-space: nowrap;
border-right: 2px solid $error;
font-family: ui-monospace, "Consolas", monospace;
font-size: 14px;
line-height: 22px;
font-weight: 700;
color: $subject-ink;
}
.dz__btn {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: $sp-1;
width: 100%;
height: 34px;
margin: 0;
padding: 0;
border: 0;
border-radius: $r-control;
background-color: $error;
color: $subject-cream;
cursor: pointer;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-weight: 800;
transition-property: background-color, color;
transition-duration: $duration;
transition-timing-function: $easing;
}
// 낱말이 정확히 맞을 때까지 버튼은 잠긴 채다
.dz__btn[aria-disabled="true"] {
background-color: $error-soft;
color: $subject-ink; // $error 글자는 4.41:1 로 미달 — 밝은 면이라 잉크로 14.68:1
cursor: default;
}
.dz__btn:focus-visible { outline: 3px solid $color; outline-offset: 3px; }
.dz__live {
position: relative;
margin: 0;
height: 16px;
font-size: 10px;
line-height: 16px;
font-weight: 600;
color: $subject-ink;
}
.dz__word {
position: absolute;
left: 0;
right: 0;
top: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0;
}
.dz:not(.is-demo) .dz__word[data-word="lock"] { opacity: 1; }
.dz:not(.is-demo) .dz__btn[aria-disabled="false"] ~ .dz__live .dz__word[data-word="lock"] { opacity: 0; }
.dz:not(.is-demo) .dz__btn[aria-disabled="false"] ~ .dz__live .dz__word[data-word="open"] { opacity: 1; }
.dz__scrim {
position: absolute;
inset: 0;
border-radius: $r-card;
background-color: $subject-ink;
opacity: 0;
visibility: hidden;
}
.dz__dialog {
position: absolute;
left: $sp-3;
right: $sp-3;
bottom: $sp-3;
display: flex;
flex-direction: column;
gap: $sp-1;
padding: $sp-3;
border-radius: $r-card;
background-color: $stage-paper;
color: $subject-ink;
box-shadow: $shadow-raised;
opacity: 0;
visibility: hidden;
}
.dz:not(.is-demo)[data-open="yes"] .dz__scrim { opacity: .78; visibility: visible; }
.dz:not(.is-demo)[data-open="yes"] .dz__dialog { opacity: 1; visibility: visible; }
.dz__dtitle {
margin: 0;
font-size: 13px;
line-height: 18px;
font-weight: 800;
color: $subject-ink;
}
.dz__dbody {
margin: 0;
font-size: 10px;
line-height: 14px;
font-weight: 500;
color: $subject-ink;
}
.dz__drow {
display: flex;
gap: $sp-2;
margin-top: $sp-1;
}
.dz__dbtn {
box-sizing: border-box;
flex: 1 1 auto;
height: 30px;
margin: 0;
padding: 0;
border: 0;
border-radius: $r-control;
cursor: pointer;
font-family: inherit;
font-size: 12px;
line-height: 16px;
font-weight: 700;
}
.dz__dbtn:focus-visible { outline: 3px solid $color; outline-offset: 2px; }
.dz__dbtn--no { background-color: $subject-cream; color: $subject-ink; }
.dz__dbtn--yes { background-color: $error; color: $subject-cream; }
// ── 자동 루프: 한 자씩 찍힘(0~55%) → 버튼 열림(58%) → 확인창 열림(70%) → 열린 채로 머문다 ──
.dz.is-demo .dz__typed {
animation-name: dz-type, dz-caret;
animation-duration: $dur-loop, 400ms;
animation-timing-function: steps(6, end), steps(1, end);
animation-iteration-count: infinite, infinite;
}
@keyframes dz-type {
0% { width: 0ch; }
55% { width: 6ch; }
100% { width: 6ch; }
}
@keyframes dz-caret {
0% { border-right-color: $error; }
50% { border-right-color: transparent; }
100% { border-right-color: transparent; }
}
.dz.is-demo .dz__btn {
animation-name: dz-unlock, dz-nudge;
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: steps(1, end), $easing;
animation-iteration-count: infinite, infinite;
}
@keyframes dz-unlock {
0% { background-color: $error-soft; color: $error; }
58% { background-color: $error; color: $subject-cream; }
100% { background-color: $error; color: $subject-cream; }
}
// 컷만으로는 움직인 프레임이 안 차서 버튼이 열리는 순간 한 번 들썩이게 한다
@keyframes dz-nudge {
0% { transform: translateY(0); }
56% { transform: translateY(0); }
60% { transform: translateY(-3px); }
66% { transform: translateY(0); }
69% { transform: translateY(2px); }
76% { transform: translateY(0); }
100% { transform: translateY(0); }
}
.dz.is-demo .dz__word {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.dz.is-demo .dz__word[data-word="lock"] { animation-name: dz-w-lock; }
.dz.is-demo .dz__word[data-word="open"] { animation-name: dz-w-open; }
@keyframes dz-w-lock {
0% { opacity: 1; }
58% { opacity: 0; }
100% { opacity: 0; }
}
@keyframes dz-w-open {
0% { opacity: 0; }
58% { opacity: 1; }
100% { opacity: 1; }
}
// 확인창은 한 프레임에 컷으로 열리고 루프가 끝날 때까지 닫지 않는다 (닫히는 프레임이 poster 로 뽑힌다)
.dz.is-demo .dz__scrim {
animation-name: dz-scrim;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes dz-scrim {
0% { opacity: 0; visibility: hidden; }
70% { opacity: .78; visibility: visible; }
100% { opacity: .78; visibility: visible; }
}
.dz.is-demo .dz__dialog {
animation-name: dz-open;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes dz-open {
0% { opacity: 0; visibility: hidden; }
70% { opacity: 1; visibility: visible; }
100% { opacity: 1; visibility: visible; }
}
.dz.is-demo .dz__dbtn--yes {
animation-name: dz-yes-pulse;
animation-duration: $dur-loop;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
}
@keyframes dz-yes-pulse {
0% { transform: scale(1); }
72% { transform: scale(1); }
82% { transform: scale(1.08); }
90% { transform: scale(1); }
100% { transform: scale(1); }
}
@media (max-width: 340px) {
.dz { gap: $sp-1; padding: $sp-2; }
.dz__head { font-size: 12px; line-height: 16px; }
.dz__desc { font-size: 10px; line-height: 13px; }
.dz__label { font-size: 9px; line-height: 12px; }
.dz__input { height: 28px; font-size: 12px; line-height: 28px; }
.dz__typed { font-size: 12px; line-height: 18px; }
.dz__btn { height: 28px; font-size: 12px; }
.dz__live { height: 14px; font-size: 9px; line-height: 14px; }
.dz__dialog { left: $sp-2; right: $sp-2; bottom: $sp-2; gap: 0; padding: $sp-2; }
.dz__dtitle { font-size: 12px; line-height: 16px; }
.dz__dbody { font-size: 9px; line-height: 12px; }
.dz__drow { margin-top: $sp-1; }
.dz__dbtn { height: 26px; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.dz.is-demo .dz__typed,
.dz.is-demo .dz__btn,
.dz.is-demo .dz__word,
.dz.is-demo .dz__scrim,
.dz.is-demo .dz__dialog,
.dz.is-demo .dz__dbtn--yes { animation: none !important; }
.dz.is-demo .dz__typed { width: 6ch !important; }
.dz.is-demo .dz__btn { background-color: $error !important; color: $subject-cream !important; }
.dz.is-demo .dz__word[data-word="open"] { opacity: 1 !important; }
.dz.is-demo .dz__scrim { opacity: .78 !important; visibility: visible !important; }
.dz.is-demo .dz__dialog { opacity: 1 !important; visibility: visible !important; }
}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);
}
.dz {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 8px;
width: min(300px, 88vw);
padding: 12px;
border-radius: 16px;
background-color: #fff7e6;
color: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.dz__ic {
display: inline-flex;
flex: 0 0 auto;
}
.dz__ic svg {
display: block;
}
.dz__head {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-size: 13px;
line-height: 18px;
font-weight: 800;
color: #c2361a;
}
.dz__desc {
margin: 0;
font-size: 11px;
line-height: 16px;
font-weight: 500;
color: #17141a;
}
.dz__label {
display: block;
font-size: 10px;
line-height: 14px;
font-weight: 700;
color: #c2361a;
}
.dz__slot {
position: relative;
display: block;
}
.dz__input {
box-sizing: border-box;
display: block;
width: 100%;
height: 32px;
margin: 0;
padding: 0 8px;
border: 0;
border-radius: 8px;
background-color: #ffffff;
color: #17141a;
font-family: ui-monospace, "Consolas", monospace;
font-size: 14px;
line-height: 32px;
font-weight: 700;
box-shadow: inset 0 0 0 2px #ffe0d8;
}
.dz__input:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 2px;
}
.dz__ghost {
position: absolute;
left: 8px;
top: 0;
bottom: 0;
display: none;
align-items: center;
}
.dz.is-demo .dz__ghost {
display: flex;
}
.dz.is-demo .dz__input {
color: transparent;
}
.dz.is-demo .dz__input::placeholder {
color: transparent;
}
.dz__typed {
display: block;
overflow: hidden;
width: 0;
white-space: nowrap;
border-right: 2px solid #c2361a;
font-family: ui-monospace, "Consolas", monospace;
font-size: 14px;
line-height: 22px;
font-weight: 700;
color: #17141a;
}
.dz__btn {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
height: 34px;
margin: 0;
padding: 0;
border: 0;
border-radius: 12px;
background-color: #c2361a;
color: #fff7e6;
cursor: pointer;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-weight: 800;
transition-property: background-color, color;
transition-duration: 220ms;
transition-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
}
.dz__btn[aria-disabled=true] {
background-color: #ffe0d8;
color: #17141a;
cursor: default;
}
.dz__btn:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 3px;
}
.dz__live {
position: relative;
margin: 0;
height: 16px;
font-size: 10px;
line-height: 16px;
font-weight: 600;
color: #17141a;
}
.dz__word {
position: absolute;
left: 0;
right: 0;
top: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0;
}
.dz:not(.is-demo) .dz__word[data-word=lock] {
opacity: 1;
}
.dz:not(.is-demo) .dz__btn[aria-disabled=false] ~ .dz__live .dz__word[data-word=lock] {
opacity: 0;
}
.dz:not(.is-demo) .dz__btn[aria-disabled=false] ~ .dz__live .dz__word[data-word=open] {
opacity: 1;
}
.dz__scrim {
position: absolute;
inset: 0;
border-radius: 16px;
background-color: #17141a;
opacity: 0;
visibility: hidden;
}
.dz__dialog {
position: absolute;
left: 12px;
right: 12px;
bottom: 12px;
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border-radius: 16px;
background-color: #ffffff;
color: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
opacity: 0;
visibility: hidden;
}
.dz:not(.is-demo)[data-open=yes] .dz__scrim {
opacity: 0.78;
visibility: visible;
}
.dz:not(.is-demo)[data-open=yes] .dz__dialog {
opacity: 1;
visibility: visible;
}
.dz__dtitle {
margin: 0;
font-size: 13px;
line-height: 18px;
font-weight: 800;
color: #17141a;
}
.dz__dbody {
margin: 0;
font-size: 10px;
line-height: 14px;
font-weight: 500;
color: #17141a;
}
.dz__drow {
display: flex;
gap: 8px;
margin-top: 4px;
}
.dz__dbtn {
box-sizing: border-box;
flex: 1 1 auto;
height: 30px;
margin: 0;
padding: 0;
border: 0;
border-radius: 12px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
line-height: 16px;
font-weight: 700;
}
.dz__dbtn:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 2px;
}
.dz__dbtn--no {
background-color: #fff7e6;
color: #17141a;
}
.dz__dbtn--yes {
background-color: #c2361a;
color: #fff7e6;
}
.dz.is-demo .dz__typed {
animation-name: dz-type, dz-caret;
animation-duration: 2s, 400ms;
animation-timing-function: steps(6, end), steps(1, end);
animation-iteration-count: infinite, infinite;
}
@keyframes dz-type {
0% {
width: 0ch;
}
55% {
width: 6ch;
}
100% {
width: 6ch;
}
}
@keyframes dz-caret {
0% {
border-right-color: #c2361a;
}
50% {
border-right-color: transparent;
}
100% {
border-right-color: transparent;
}
}
.dz.is-demo .dz__btn {
animation-name: dz-unlock, dz-nudge;
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite, infinite;
}
@keyframes dz-unlock {
0% {
background-color: #ffe0d8;
color: #c2361a;
}
58% {
background-color: #c2361a;
color: #fff7e6;
}
100% {
background-color: #c2361a;
color: #fff7e6;
}
}
@keyframes dz-nudge {
0% {
transform: translateY(0);
}
56% {
transform: translateY(0);
}
60% {
transform: translateY(-3px);
}
66% {
transform: translateY(0);
}
69% {
transform: translateY(2px);
}
76% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}
.dz.is-demo .dz__word {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.dz.is-demo .dz__word[data-word=lock] {
animation-name: dz-w-lock;
}
.dz.is-demo .dz__word[data-word=open] {
animation-name: dz-w-open;
}
@keyframes dz-w-lock {
0% {
opacity: 1;
}
58% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@keyframes dz-w-open {
0% {
opacity: 0;
}
58% {
opacity: 1;
}
100% {
opacity: 1;
}
}
.dz.is-demo .dz__scrim {
animation-name: dz-scrim;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes dz-scrim {
0% {
opacity: 0;
visibility: hidden;
}
70% {
opacity: 0.78;
visibility: visible;
}
100% {
opacity: 0.78;
visibility: visible;
}
}
.dz.is-demo .dz__dialog {
animation-name: dz-open;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes dz-open {
0% {
opacity: 0;
visibility: hidden;
}
70% {
opacity: 1;
visibility: visible;
}
100% {
opacity: 1;
visibility: visible;
}
}
.dz.is-demo .dz__dbtn--yes {
animation-name: dz-yes-pulse;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes dz-yes-pulse {
0% {
transform: scale(1);
}
72% {
transform: scale(1);
}
82% {
transform: scale(1.08);
}
90% {
transform: scale(1);
}
100% {
transform: scale(1);
}
}
@media (max-width: 340px) {
.dz {
gap: 4px;
padding: 8px;
}
.dz__head {
font-size: 12px;
line-height: 16px;
}
.dz__desc {
font-size: 10px;
line-height: 13px;
}
.dz__label {
font-size: 9px;
line-height: 12px;
}
.dz__input {
height: 28px;
font-size: 12px;
line-height: 28px;
}
.dz__typed {
font-size: 12px;
line-height: 18px;
}
.dz__btn {
height: 28px;
font-size: 12px;
}
.dz__live {
height: 14px;
font-size: 9px;
line-height: 14px;
}
.dz__dialog {
left: 8px;
right: 8px;
bottom: 8px;
gap: 0;
padding: 8px;
}
.dz__dtitle {
font-size: 12px;
line-height: 16px;
}
.dz__dbody {
font-size: 9px;
line-height: 12px;
}
.dz__drow {
margin-top: 4px;
}
.dz__dbtn {
height: 26px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.dz.is-demo .dz__typed,
.dz.is-demo .dz__btn,
.dz.is-demo .dz__word,
.dz.is-demo .dz__scrim,
.dz.is-demo .dz__dialog,
.dz.is-demo .dz__dbtn--yes {
animation: none !important;
}
.dz.is-demo .dz__typed {
width: 6ch !important;
}
.dz.is-demo .dz__btn {
background-color: #c2361a !important;
color: #fff7e6 !important;
}
.dz.is-demo .dz__word[data-word=open] {
opacity: 1 !important;
}
.dz.is-demo .dz__scrim {
opacity: 0.78 !important;
visibility: visible !important;
}
.dz.is-demo .dz__dialog {
opacity: 1 !important;
visibility: visible !important;
}
}// 실제 동작: DELETE 를 정확히 쳐야 버튼이 열리고, 열린 확인창 안에 초점이 갇힌다 (Escape 로 닫는다)
var root = document.querySelector('.dz');
var input = document.getElementById('dz-input');
var delBtn = document.getElementById('dz-del');
var dialog = document.getElementById('dz-dialog');
var noBtn = document.getElementById('dz-no');
var yesBtn = document.getElementById('dz-yes');
var live = document.getElementById('dz-live');
var word = 'DELETE';
function isLocked() { return input.value !== word; }
function openDialog(on) {
root.setAttribute('data-open', on ? 'yes' : 'no');
(on ? noBtn : delBtn).focus({ preventScroll: true });
}
input.addEventListener('input', function () {
root.classList.remove('is-demo'); // 사람이 만졌으니 자동 루프를 끈다
delBtn.setAttribute('aria-disabled', String(isLocked()));
live.textContent = isLocked() ? 'DELETE 를 정확히 입력해야 열립니다' : '삭제 버튼이 열렸습니다';
});
delBtn.addEventListener('click', function () { if (!isLocked()) openDialog(true); });
noBtn.addEventListener('click', function () { openDialog(false); });
yesBtn.addEventListener('click', function () { openDialog(false); live.textContent = '삭제 요청을 보냈습니다'; });
dialog.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { openDialog(false); return; }
if (e.key !== 'Tab') return;
e.preventDefault(); // 초점이 확인창 밖으로 못 나간다
(document.activeElement === noBtn ? yesBtn : noBtn).focus({ preventScroll: true });
});import { useState, useRef, useEffect } from "react";
import type { CSSProperties } from "react";
import styles from "./DeleteAccountZone.module.scss";
export type DeleteAccountZoneProps = {
word?: string;
head?: string;
desc?: string;
label?: string;
action?: string;
logs?: { lock: string; open: string; sent: string };
dialog?: { title: string; body: string; no: string; yes: string };
duration?: number;
easing?: string;
color?: string;
};
// ambient d.ts 에 React 이벤트 제네릭이 없다 — 파일 안 로컬 타입으로 받는다
type KeyEvt = { key: string; preventDefault(): void };
type TextEvt = { target: { value: string } };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const WARNING = (
<g fill="currentColor">
<path d="M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91" opacity=".2" />
<path d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12" />
</g>
);
const TRASH = (
<g fill="currentColor">
<path d="M200 56v152a8 8 0 0 1-8 8H64a8 8 0 0 1-8-8V56Z" opacity=".2" />
<path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16M96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0m48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0" />
</g>
);
export default function DeleteAccountZone({
word = "DELETE",
head = "Danger zone",
desc = "Deleting the account removes your posts and files with it. Type DELETE exactly to confirm.",
label = "Confirm phrase",
action = "Delete account",
logs = {
lock: "Type DELETE exactly to unlock",
open: "The delete button is unlocked",
sent: "Delete request sent",
},
dialog = {
title: "Delete for real?",
body: "You can undo this within 30 days.",
no: "Cancel",
yes: "Yes, delete",
},
duration = 220,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
}: DeleteAccountZoneProps) {
const [demo, setDemo] = useState(true);
const [typed, setTyped] = useState("");
const [open, setOpen] = useState(false);
const [live, setLive] = useState("");
const noRef = useRef<HTMLButtonElement>(null);
const yesRef = useRef<HTMLButtonElement>(null);
const delRef = useRef<HTMLButtonElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const isLocked = typed !== word;
// 초점은 확인창이 열리면 안으로, 닫히면 삭제 버튼으로 돌아온다. 언제나 preventScroll
useEffect(() => {
if (!demo) {
const t = open ? noRef.current : delRef.current;
if (t !== null) t.focus({ preventScroll: true });
}
}, [open, demo]);
function handleInput(e: TextEvt) {
setDemo(false); // 사람이 만졌으니 자동 루프를 끈다
setTyped(e.target.value);
setLive(e.target.value !== word ? logs.lock : logs.open);
}
// 초점이 확인창 밖으로 못 나간다
function trapFocus(e: KeyEvt) {
if (e.key === "Escape") {
setOpen(false);
return;
}
if (e.key !== "Tab") return;
e.preventDefault();
const current = noRef.current;
const next = current !== null && document.activeElement === current ? yesRef.current : noRef.current;
if (next !== null) next.focus({ preventScroll: true });
}
return (
<section
className={`${styles.dz} ${demo ? styles.isDemo : ""}`}
data-open={open ? "yes" : "no"}
style={vars}
>
<p className={styles.head}>
<span className={styles.ic} aria-hidden="true">
<svg viewBox="0 0 256 256" width="16" height="16">{WARNING}</svg>
</span>
{head}
</p>
<p className={styles.desc}>{desc}</p>
<label className={styles.label} htmlFor="dz-input">{label}</label>
<span className={styles.slot}>
<input
className={styles.input}
id="dz-input"
type="text"
autoComplete="off"
spellCheck={false}
placeholder={word}
value={typed}
onChange={handleInput}
/>
<span className={styles.ghost} aria-hidden="true">
<span className={styles.typed}>{word}</span>
</span>
</span>
<button
className={styles.btn}
type="button"
ref={delRef}
aria-disabled={isLocked}
onClick={() => { if (!isLocked) setOpen(true); }}
>
<span className={styles.ic} aria-hidden="true">
<svg viewBox="0 0 256 256" width="14" height="14">{TRASH}</svg>
</span>
{action}
</button>
<p className={styles.live} role="status" aria-live="polite">
{live ? (
live
) : (
<>
<span className={styles.word} data-word="lock">{logs.lock}</span>
<span className={styles.word} data-word="open">{logs.open}</span>
</>
)}
</p>
<span className={styles.scrim} aria-hidden="true" />
<div
className={styles.dialog}
role="alertdialog"
aria-modal="true"
aria-labelledby="dz-dtitle"
aria-describedby="dz-dbody"
onKeyDown={trapFocus}
>
<p className={styles.dtitle} id="dz-dtitle">{dialog.title}</p>
<p className={styles.dbody} id="dz-dbody">{dialog.body}</p>
<span className={styles.drow}>
<button
className={`${styles.dbtn} ${styles.dbtnNo}`}
type="button"
ref={noRef}
onClick={() => setOpen(false)}
>
{dialog.no}
</button>
<button
className={`${styles.dbtn} ${styles.dbtnYes}`}
type="button"
ref={yesRef}
onClick={() => { setOpen(false); setLive(logs.sent); }}
>
{dialog.yes}
</button>
</span>
</div>
</section>
);
}09 글자를 따라 쳐야 열리는 계정 삭제 — 측정 누적면적 38.426% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 계정 보안 설정 UI 9종 — 로그인한 뒤의 화면
이 데모가 속한 글
계정 보안 설정 UI 9종 — 로그인한 뒤의 화면계정 보안 설정은 로그인을 통과한 사람이 마주하는, 접속 중인 기기와 2단계 인증, 비밀번호를 직접 만지는 화면입니다. 아홉 개는 전부 실제로 돌아갑니다. 기기 줄은 정말로 접히고, 백업 코드는 정말로 클립보드에 들 이 글의 데모 전부 보기


