07Review screen that lays everything out before you send
The last screen rewrites the values from earlier steps group by group, and each group carries a pencil button holding that step number, so only the pressed group lifts for a moment and settles.
Best forThe order confirmation right before payment, the last pass over an application before submitting
aria-labelscalesteps(1, end)

<main class="stage">
<section class="rv is-demo" id="rv" aria-label="Final review before sending">
<p class="rv__h">Last check</p>
<div class="rv__g" data-g="1">
<div class="rv__top">
<p class="rv__name">Address</p>
<button class="rv__pen" type="button" data-step="1" aria-label="Edit the delivery address" aria-pressed="false">
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M221.66 90.34L192 120l-56-56l29.66-29.66a8 8 0 0 1 11.31 0L221.66 79a8 8 0 0 1 0 11.34" opacity=".2"/><path d="m227.31 73.37l-44.68-44.69a16 16 0 0 0-22.63 0L36.69 152A15.86 15.86 0 0 0 32 163.31V208a16 16 0 0 0 16 16h44.69a15.86 15.86 0 0 0 11.31-4.69L227.31 96a16 16 0 0 0 0-22.63M92.69 208H48v-44.69l88-88L180.69 120ZM192 108.68L147.31 64l24-24L216 84.68Z"/></g></svg>
</button>
</div>
<dl class="rv__kv">
<div class="rv__row">
<dt class="rv__k">Recipient</dt>
<dd class="rv__dd" data-label="Recipient"><span class="rv__val">Hong Gildong</span></dd>
</div>
<div class="rv__row">
<dt class="rv__k">Address</dt>
<dd class="rv__dd" data-label="Address"><span class="rv__val">12-34 Yeoksam-dong, Gangnam-gu, Seoul</span></dd>
</div>
</dl>
</div>
<div class="rv__g" data-g="2">
<div class="rv__top">
<p class="rv__name">Phone</p>
<button class="rv__pen" type="button" data-step="2" aria-label="Edit the contact details" aria-pressed="false">
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M221.66 90.34L192 120l-56-56l29.66-29.66a8 8 0 0 1 11.31 0L221.66 79a8 8 0 0 1 0 11.34" opacity=".2"/><path d="m227.31 73.37l-44.68-44.69a16 16 0 0 0-22.63 0L36.69 152A15.86 15.86 0 0 0 32 163.31V208a16 16 0 0 0 16 16h44.69a15.86 15.86 0 0 0 11.31-4.69L227.31 96a16 16 0 0 0 0-22.63M92.69 208H48v-44.69l88-88L180.69 120ZM192 108.68L147.31 64l24-24L216 84.68Z"/></g></svg>
</button>
</div>
<dl class="rv__kv">
<div class="rv__row">
<dt class="rv__k">Phone</dt>
<dd class="rv__dd" data-label="Phone"><span class="rv__val">+1 555 0134</span></dd>
</div>
<div class="rv__row">
<dt class="rv__k">Email</dt>
<dd class="rv__dd" data-label="Email"><span class="rv__val">[email protected]</span></dd>
</div>
</dl>
</div>
<div class="rv__g" data-g="3">
<div class="rv__top">
<p class="rv__name">Payment</p>
<button class="rv__pen" type="button" data-step="3" aria-label="Edit the payment method" aria-pressed="false">
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M221.66 90.34L192 120l-56-56l29.66-29.66a8 8 0 0 1 11.31 0L221.66 79a8 8 0 0 1 0 11.34" opacity=".2"/><path d="m227.31 73.37l-44.68-44.69a16 16 0 0 0-22.63 0L36.69 152A15.86 15.86 0 0 0 32 163.31V208a16 16 0 0 0 16 16h44.69a15.86 15.86 0 0 0 11.31-4.69L227.31 96a16 16 0 0 0 0-22.63M92.69 208H48v-44.69l88-88L180.69 120ZM192 108.68L147.31 64l24-24L216 84.68Z"/></g></svg>
</button>
</div>
<dl class="rv__kv">
<div class="rv__row">
<dt class="rv__k">Card</dt>
<dd class="rv__dd" data-label="Card"><span class="rv__val">Personal card · 4444</span></dd>
</div>
<div class="rv__row">
<dt class="rv__k">Installments</dt>
<dd class="rv__dd" data-label="Installments"><span class="rv__val">Pay in full</span></dd>
</div>
</dl>
</div>
</section>
</main>@use "sass:color";
@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-cream;
@include stage($stage-orange);
$line: color.mix($subject-cream, $stage-orange, 24%);
$soft: color.mix($subject-cream, $stage-orange, 10%);
$dim: rgba(255, 247, 230, .72);
$muted: rgba(255, 247, 230, .5);
.rv {
box-sizing: border-box;
display: grid;
gap: $sp-1;
width: min(320px, 92vw);
padding: $sp-2;
border: 1px solid $line;
border-radius: $r-card;
background: $stage-orange;
color: $subject-cream;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.rv__h { margin: 0; font-size: 11px; font-weight: 700; letter-spacing: .04em; color: $subject-ink; }
// ── 값 묶음 하나 ────────────────────────────────
.rv__g {
box-sizing: border-box;
padding: $sp-2;
border: 1px solid $line;
border-radius: $r-sm;
background: transparent;
transition: border-color $duration $easing, background-color $duration $easing;
}
// 편집 상태 — 눌린 묶음만 살짝 들렀다 가라앉는다(scale 한 번 재생)
.rv__g.is-edit {
border-color: $color;
background: $soft;
animation-name: rv-lift;
animation-duration: 600ms;
animation-timing-function: $easing;
animation-iteration-count: 1;
}
@keyframes rv-lift {
0% { transform: scale(1); }
40% { transform: scale(1.04); }
100% { transform: scale(1); }
}
.rv__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: $sp-2;
margin: 0 0 $sp-1;
}
.rv__name { margin: 0; font-size: 11px; font-weight: 700; color: $subject-ink; }
.rv__pen {
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
margin: 0;
padding: 0;
border: 1px solid $line;
border-radius: 50%;
background: transparent;
color: $dim;
cursor: pointer;
transition: background-color $duration $easing, border-color $duration $easing, color $duration $easing;
}
.rv__pen svg { width: 12px; height: 12px; }
.rv__pen:hover { border-color: $color; color: $subject-cream; }
.rv__pen:focus-visible { outline: 2px solid $color; outline-offset: 1px; }
.rv__g.is-edit .rv__pen { background: $color; border-color: $color; color: $stage-orange; }
.rv__kv { display: grid; margin: 0; }
.rv__row {
display: grid;
grid-template-columns: 64px 1fr;
gap: $sp-2;
align-items: baseline;
}
.rv__k { font-size: 11px; color: $subject-ink; }
.rv__dd {
margin: 0;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 11px;
color: $subject-cream;
}
// 정적 상태(사람이 조작하는 길) — 검토 목록은 값이 적혀 있는 채로 산다
.rv__val {
color: $subject-ink;
border-radius: $r-xs;
transition: background-color $duration $easing, box-shadow $duration $easing;
}
// 편집으로 진짜 생기는 칸 — 값이 input 으로 바뀐다
.rv__inp {
box-sizing: border-box;
width: 100%;
min-width: 0;
margin: 0;
padding: 0 $sp-1;
border: 0;
border-bottom: 1px solid $color;
border-radius: $r-xs $r-xs 0 0;
background: rgba(255, 247, 230, .14);
color: $subject-ink;
font-family: inherit;
font-size: 11px;
line-height: 1.4;
}
.rv__inp:focus-visible { outline: 2px solid $color; outline-offset: 1px; }
// ── 격자 미리보기 전용 2초 루프 ──────────────────
// 묶음이 하나씩 scale 로 들렀다 놓이고, 편집 상태 전환은 steps(1, end) 로 한 프레임에 컷.
// 검토 목록(주인공)은 루프 안에서 지우지 않는다.
@keyframes rv-lift-1 {
0%, 4% { transform: scale(1); }
10% { transform: scale(1.04); }
16%, 100% { transform: scale(1); }
}
@keyframes rv-lift-2 {
0%, 32% { transform: scale(1); }
38% { transform: scale(1.04); }
44%, 100% { transform: scale(1); }
}
@keyframes rv-lift-3 {
0%, 60% { transform: scale(1); }
66% { transform: scale(1.04); }
72%, 100% { transform: scale(1); }
}
// 편집 상태는 한 번 켜지면 루프 끝까지 유지되고, 처음으로 돌아가는 컷은
// 애니메이션 경계(100% → 0%) 자체다 — 세 묶음이 한꺼번에 꺼지는 프레임이 poster 로 뽑히는 일을 막는다
@keyframes rv-edit-1 {
0%, 18% { border-color: $line; background: transparent; }
19%, 100% { border-color: $color; background: $soft; }
}
@keyframes rv-edit-2 {
0%, 48% { border-color: $line; background: transparent; }
49%, 100% { border-color: $color; background: $soft; }
}
@keyframes rv-edit-3 {
0%, 78% { border-color: $line; background: transparent; }
79%, 100% { border-color: $color; background: $soft; }
}
@keyframes rv-pen-1 {
0%, 18% { background: transparent; color: $dim; }
19%, 100% { background: $color; color: $stage-orange; }
}
@keyframes rv-pen-2 {
0%, 48% { background: transparent; color: $dim; }
49%, 100% { background: $color; color: $stage-orange; }
}
@keyframes rv-pen-3 {
0%, 78% { background: transparent; color: $dim; }
79%, 100% { background: $color; color: $stage-orange; }
}
@keyframes rv-field-1 {
0%, 18% { background: transparent; box-shadow: none; }
19%, 100% { background: rgba(255, 247, 230, .14); box-shadow: 0 1px 0 $color; }
}
@keyframes rv-field-2 {
0%, 48% { background: transparent; box-shadow: none; }
49%, 100% { background: rgba(255, 247, 230, .14); box-shadow: 0 1px 0 $color; }
}
@keyframes rv-field-3 {
0%, 78% { background: transparent; box-shadow: none; }
79%, 100% { background: rgba(255, 247, 230, .14); box-shadow: 0 1px 0 $color; }
}
.rv.is-demo {
.rv__g[data-g="1"] { animation-name: rv-lift-1, rv-edit-1; }
.rv__g[data-g="2"] { animation-name: rv-lift-2, rv-edit-2; }
.rv__g[data-g="3"] { animation-name: rv-lift-3, rv-edit-3; }
.rv__g {
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: $easing, steps(1, end);
animation-iteration-count: infinite, infinite;
}
.rv__g[data-g="1"] .rv__pen { animation-name: rv-pen-1; }
.rv__g[data-g="2"] .rv__pen { animation-name: rv-pen-2; }
.rv__g[data-g="3"] .rv__pen { animation-name: rv-pen-3; }
.rv__pen {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.rv__g[data-g="1"] .rv__val { animation-name: rv-field-1; }
.rv__g[data-g="2"] .rv__val { animation-name: rv-field-2; }
.rv__g[data-g="3"] .rv__val { animation-name: rv-field-3; }
.rv__val {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
}
// 좁은 폭에서는 각 묶음의 첫 값만 보인다 — 320px 세로 예산(174px) 안에 들어가게
@media (max-width: 340px) {
.rv { padding: $sp-2; gap: $sp-1; }
.rv__g { padding: $sp-1; }
.rv__top { margin-bottom: $sp-1; }
.rv__pen { width: 16px; height: 16px; }
.rv__pen svg { width: 10px; height: 10px; }
.rv__row { grid-template-columns: 52px 1fr; gap: $sp-1; }
.rv__name, .rv__k, .rv__dd, .rv__inp { font-size: 10px; }
.rv__kv .rv__row:nth-child(2) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.rv.is-demo .rv__g,
.rv.is-demo .rv__pen,
.rv.is-demo .rv__val { animation: none !important; transform: none !important; }
// 움직임만 끄고 판정 결과는 남긴다 — 첫 묶음이 편집 상태인 장을 보여 준다
.rv.is-demo .rv__g[data-g="1"] { border-color: $color !important; background: $soft !important; }
.rv.is-demo .rv__g[data-g="1"] .rv__pen { background: $color !important; border-color: $color !important; color: $stage-orange !important; }
.rv.is-demo .rv__g[data-g="1"] .rv__val { background: rgba(255, 247, 230, .14) !important; box-shadow: 0 1px 0 $color !important; }
}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);
}
.rv {
box-sizing: border-box;
display: grid;
gap: 4px;
width: min(320px, 92vw);
padding: 8px;
border: 1px solid rgb(100%, 46.1960784314%, 30.8862745098%);
border-radius: 16px;
background: #ff4d1f;
color: #fff7e6;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.rv__h {
margin: 0;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
color: #17141a;
}
.rv__g {
box-sizing: border-box;
padding: 8px;
border: 1px solid rgb(100%, 46.1960784314%, 30.8862745098%);
border-radius: 8px;
background: transparent;
transition: border-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rv__g.is-edit {
border-color: #fff7e6;
background: rgb(100%, 36.862745098%, 19.9607843137%);
animation-name: rv-lift;
animation-duration: 600ms;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: 1;
}
@keyframes rv-lift {
0% {
transform: scale(1);
}
40% {
transform: scale(1.04);
}
100% {
transform: scale(1);
}
}
.rv__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 0 0 4px;
}
.rv__name {
margin: 0;
font-size: 11px;
font-weight: 700;
color: #17141a;
}
.rv__pen {
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
margin: 0;
padding: 0;
border: 1px solid rgb(100%, 46.1960784314%, 30.8862745098%);
border-radius: 50%;
background: transparent;
color: rgba(255, 247, 230, 0.72);
cursor: pointer;
transition: background-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rv__pen svg {
width: 12px;
height: 12px;
}
.rv__pen:hover {
border-color: #fff7e6;
color: #fff7e6;
}
.rv__pen:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 1px;
}
.rv__g.is-edit .rv__pen {
background: #fff7e6;
border-color: #fff7e6;
color: #ff4d1f;
}
.rv__kv {
display: grid;
margin: 0;
}
.rv__row {
display: grid;
grid-template-columns: 64px 1fr;
gap: 8px;
align-items: baseline;
}
.rv__k {
font-size: 11px;
color: #17141a;
}
.rv__dd {
margin: 0;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 11px;
color: #fff7e6;
}
.rv__val {
color: #17141a;
border-radius: 4px;
transition: background-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rv__inp {
box-sizing: border-box;
width: 100%;
min-width: 0;
margin: 0;
padding: 0 4px;
border: 0;
border-bottom: 1px solid #fff7e6;
border-radius: 4px 4px 0 0;
background: rgba(255, 247, 230, 0.14);
color: #17141a;
font-family: inherit;
font-size: 11px;
line-height: 1.4;
}
.rv__inp:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 1px;
}
@keyframes rv-lift-1 {
0%, 4% {
transform: scale(1);
}
10% {
transform: scale(1.04);
}
16%, 100% {
transform: scale(1);
}
}
@keyframes rv-lift-2 {
0%, 32% {
transform: scale(1);
}
38% {
transform: scale(1.04);
}
44%, 100% {
transform: scale(1);
}
}
@keyframes rv-lift-3 {
0%, 60% {
transform: scale(1);
}
66% {
transform: scale(1.04);
}
72%, 100% {
transform: scale(1);
}
}
@keyframes rv-edit-1 {
0%, 18% {
border-color: rgb(100%, 46.1960784314%, 30.8862745098%);
background: transparent;
}
19%, 100% {
border-color: #fff7e6;
background: rgb(100%, 36.862745098%, 19.9607843137%);
}
}
@keyframes rv-edit-2 {
0%, 48% {
border-color: rgb(100%, 46.1960784314%, 30.8862745098%);
background: transparent;
}
49%, 100% {
border-color: #fff7e6;
background: rgb(100%, 36.862745098%, 19.9607843137%);
}
}
@keyframes rv-edit-3 {
0%, 78% {
border-color: rgb(100%, 46.1960784314%, 30.8862745098%);
background: transparent;
}
79%, 100% {
border-color: #fff7e6;
background: rgb(100%, 36.862745098%, 19.9607843137%);
}
}
@keyframes rv-pen-1 {
0%, 18% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
19%, 100% {
background: #fff7e6;
color: #ff4d1f;
}
}
@keyframes rv-pen-2 {
0%, 48% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
49%, 100% {
background: #fff7e6;
color: #ff4d1f;
}
}
@keyframes rv-pen-3 {
0%, 78% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
79%, 100% {
background: #fff7e6;
color: #ff4d1f;
}
}
@keyframes rv-field-1 {
0%, 18% {
background: transparent;
box-shadow: none;
}
19%, 100% {
background: rgba(255, 247, 230, 0.14);
box-shadow: 0 1px 0 #fff7e6;
}
}
@keyframes rv-field-2 {
0%, 48% {
background: transparent;
box-shadow: none;
}
49%, 100% {
background: rgba(255, 247, 230, 0.14);
box-shadow: 0 1px 0 #fff7e6;
}
}
@keyframes rv-field-3 {
0%, 78% {
background: transparent;
box-shadow: none;
}
79%, 100% {
background: rgba(255, 247, 230, 0.14);
box-shadow: 0 1px 0 #fff7e6;
}
}
.rv.is-demo .rv__g[data-g="1"] {
animation-name: rv-lift-1, rv-edit-1;
}
.rv.is-demo .rv__g[data-g="2"] {
animation-name: rv-lift-2, rv-edit-2;
}
.rv.is-demo .rv__g[data-g="3"] {
animation-name: rv-lift-3, rv-edit-3;
}
.rv.is-demo .rv__g {
animation-duration: 2s, 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1), steps(1, end);
animation-iteration-count: infinite, infinite;
}
.rv.is-demo .rv__g[data-g="1"] .rv__pen {
animation-name: rv-pen-1;
}
.rv.is-demo .rv__g[data-g="2"] .rv__pen {
animation-name: rv-pen-2;
}
.rv.is-demo .rv__g[data-g="3"] .rv__pen {
animation-name: rv-pen-3;
}
.rv.is-demo .rv__pen {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.rv.is-demo .rv__g[data-g="1"] .rv__val {
animation-name: rv-field-1;
}
.rv.is-demo .rv__g[data-g="2"] .rv__val {
animation-name: rv-field-2;
}
.rv.is-demo .rv__g[data-g="3"] .rv__val {
animation-name: rv-field-3;
}
.rv.is-demo .rv__val {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.rv {
padding: 8px;
gap: 4px;
}
.rv__g {
padding: 4px;
}
.rv__top {
margin-bottom: 4px;
}
.rv__pen {
width: 16px;
height: 16px;
}
.rv__pen svg {
width: 10px;
height: 10px;
}
.rv__row {
grid-template-columns: 52px 1fr;
gap: 4px;
}
.rv__name, .rv__k, .rv__dd, .rv__inp {
font-size: 10px;
}
.rv__kv .rv__row:nth-child(2) {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.rv.is-demo .rv__g,
.rv.is-demo .rv__pen,
.rv.is-demo .rv__val {
animation: none !important;
transform: none !important;
}
.rv.is-demo .rv__g[data-g="1"] {
border-color: #fff7e6 !important;
background: rgb(100%, 36.862745098%, 19.9607843137%) !important;
}
.rv.is-demo .rv__g[data-g="1"] .rv__pen {
background: #fff7e6 !important;
border-color: #fff7e6 !important;
color: #ff4d1f !important;
}
.rv.is-demo .rv__g[data-g="1"] .rv__val {
background: rgba(255, 247, 230, 0.14) !important;
box-shadow: 0 1px 0 #fff7e6 !important;
}
}// 실제 동작: 연필 단추를 누른 묶음의 값이 진짜 input 으로 바뀌고,
// 다시 누르면 고친 값을 텍스트로 되돌려 적는다(화면만 바꾸는 흉내가 아니다).
var rv = document.getElementById('rv');
var groups = [].slice.call(rv.querySelectorAll('.rv__g'));
groups.forEach(function (g) {
var pen = g.querySelector('.rv__pen');
pen.addEventListener('click', function () {
rv.classList.remove('is-demo');
var editing = g.classList.toggle('is-edit');
pen.setAttribute('aria-pressed', editing ? 'true' : 'false');
g.querySelectorAll('.rv__dd').forEach(function (dd) {
var old = dd.querySelector('input');
if (editing && !old) {
var inp = document.createElement('input');
inp.className = 'rv__inp';
inp.value = dd.textContent.trim();
inp.setAttribute('aria-label', dd.getAttribute('data-label'));
dd.textContent = '';
dd.appendChild(inp);
} else if (!editing && old) {
dd.textContent = old.value.trim() || '—';
}
});
var first = g.querySelector('input');
if (editing && first) first.focus({ preventScroll: true });
});
g.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && g.classList.contains('is-edit')) { e.preventDefault(); pen.click(); }
});
});import { useState, useRef, useEffect } from "react";
import type { CSSProperties } from "react";
import styles from "./FinalReviewEdit.module.scss";
export type ReviewField = { label: string; value: string };
export type ReviewGroup = {
name: string;
editLabel: string;
fields: ReviewField[];
};
export type FinalReviewEditProps = {
heading?: string;
groups?: ReviewGroup[];
groupLabel?: string;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type FocusEl = { focus(opt: { preventScroll: boolean }): void };
type KeyEvt = { key: string; preventDefault(): void };
type InpEvt = { target: { value: string } };
const DEFAULT_GROUPS: ReviewGroup[] = [
{
name: "Delivery",
editLabel: "Edit the delivery address",
fields: [
{ label: "Recipient", value: "Hong Gildong" },
{ label: "Address", value: "12-34 Yeoksam-dong, Gangnam-gu, Seoul" },
],
},
{
name: "Contact",
editLabel: "Edit the contact details",
fields: [
{ label: "Phone", value: "010-2345-6789" },
{ label: "Email", value: "[email protected]" },
],
},
{
name: "Payment",
editLabel: "Edit the payment method",
fields: [
{ label: "Card", value: "Personal card · 4444" },
{ label: "Plan", value: "Pay in full" },
],
},
];
function Pencil() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M221.66 90.34L192 120l-56-56l29.66-29.66a8 8 0 0 1 11.31 0L221.66 79a8 8 0 0 1 0 11.34" opacity=".2" />
<path d="m227.31 73.37l-44.68-44.69a16 16 0 0 0-22.63 0L36.69 152A15.86 15.86 0 0 0 32 163.31V208a16 16 0 0 0 16 16h44.69a15.86 15.86 0 0 0 11.31-4.69L227.31 96a16 16 0 0 0 0-22.63M92.69 208H48v-44.69l88-88L180.69 120ZM192 108.68L147.31 64l24-24L216 84.68Z" />
</g>
</svg>
);
}
export default function FinalReviewEdit({
heading = "Last check",
groups = DEFAULT_GROUPS,
groupLabel = "Final review before sending",
demo = true,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#fff7e6",
}: FinalReviewEditProps) {
const [editing, setEditing] = useState<number | null>(null);
const [values, setValues] = useState<string[][]>(groups.map((g) => g.fields.map((f) => f.value)));
const [live, setLive] = useState(demo);
const firstBox = useRef<FocusEl | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
// 편집에 들어가면 첫 칸에 초점 — iframe 안이라 preventScroll 이 필수
useEffect(() => {
if (editing !== null) {
const el = firstBox.current;
if (el) el.focus({ preventScroll: true });
}
}, [editing]);
// 연필: 눌린 묶음의 값이 진짜 input 으로 바뀌고, 다시 누르면 고친 값이 텍스트로 남는다
function press(gi: number) {
setLive(false);
setEditing((prev) => (prev === gi ? null : gi));
}
const cls = [styles.rv, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<p className={styles.h}>{heading}</p>
{groups.map((g, gi) => {
const isEdit = editing === gi;
return (
<div key={g.name} className={[styles.g, isEdit ? styles.isEdit : ""].filter(Boolean).join(" ")} data-g={gi + 1}>
<div className={styles.top}>
<p className={styles.name}>{g.name}</p>
<button
className={styles.pen}
type="button"
data-step={gi + 1}
aria-label={g.editLabel}
aria-pressed={isEdit}
onClick={() => press(gi)}
>
<Pencil />
</button>
</div>
<dl className={styles.kv}>
{g.fields.map((f, fi) => (
<div key={f.label} className={styles.row}>
<dt className={styles.k}>{f.label}</dt>
<dd className={styles.dd}>
{isEdit ? (
<input
className={styles.inp}
value={values[gi][fi]}
aria-label={f.label}
ref={fi === 0 ? (el: FocusEl | null) => { firstBox.current = el; } : undefined}
onChange={(e: InpEvt) => {
setValues((prev) => prev.map((row, n) => (n === gi ? row.map((v, m) => (m === fi ? e.target.value : v)) : row)));
}}
onKeyDown={(e: KeyEvt) => {
if (e.key === "Enter") { e.preventDefault(); setEditing(null); }
}}
/>
) : (
<span className={styles.val}>{values[gi][fi] || "—"}</span>
)}
</dd>
</div>
))}
</dl>
</div>
);
})}
</section>
);
}07 Review screen that lays everything out before you send — Measured area 45.66% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · 9 Multi Step Form UI Patterns — Branch and Resume
The post this demo lives in
9 Multi Step Form UI Patterns — Branch and ResumeA multi step form ui splits one long form into screens and collects answers in order. All nine read real values, run rea See all demos in this post
If you like this effect - similar demos
Inputs03 Inline hint that flips while you type9 Form Validation UI Patterns — As You Type
Inputs05 Payment method card that opens only the option you pickedCheckout Page Design — 9 Cart Patterns, No Library
Inputs06 Typo suggest — computing edit distance to float a 'did you mean?' rowAutocomplete Input: 9 Search Dropdowns