04Resume strip that remembers where you stopped
Every keystroke writes the step number and the values into localStorage, and on your next visit a strip reading resume from step 3 drops in from the top and takes you back there.
Best forLong applications people abandon midway, booking forms that must survive a closed tab
localStoragearia-livetranslateY

<main class="stage">
<section class="rb is-demo" id="rb" aria-label="Application that remembers where you stopped">
<div class="rb__bar" id="rb-bar" role="status" aria-live="polite">
<span class="rb__bar-i" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2"/><path d="M136 80v43.47l36.12 21.67a8 8 0 0 1-8.24 13.72l-40-24A8 8 0 0 1 120 128V80a8 8 0 0 1 16 0m-8-48a95.44 95.44 0 0 0-67.92 28.15C52.81 67.51 46.35 74.59 40 82V64a8 8 0 0 0-16 0v40a8 8 0 0 0 8 8h40a8 8 0 0 0 0-16H49c7.15-8.42 14.27-16.35 22.39-24.57a80 80 0 1 1 1.66 114.75a8 8 0 1 0-11 11.64A96 96 0 1 0 128 32"/></g></svg></span>
<p class="rb__bar-t">You have an unfinished draft</p>
<span class="rb__bar-a">
<button class="rb__b rb__b--go" type="button" id="rb-resume" data-t="Resume from step {n}">Resume from step 3</button>
<button class="rb__b" type="button" id="rb-fresh">Start over</button>
</span>
</div>
<div class="rb__form">
<div class="rb__head">
<p class="rb__h">
<span class="rb__now"><b class="rb__d" data-d="1">1</b><b class="rb__d" data-d="3">3</b></span> / 4 · Application
</p>
<p class="rb__save" id="rb-save">Saved automatically</p>
</div>
<div class="rb__dots" aria-hidden="true">
<span class="rb__dot" data-dot="1"><span class="rb__dot-f"></span></span>
<span class="rb__dot" data-dot="2"><span class="rb__dot-f"></span></span>
<span class="rb__dot" data-dot="3"><span class="rb__dot-f"></span></span>
<span class="rb__dot" data-dot="4"><span class="rb__dot-f"></span></span>
</div>
<input class="rb__in" id="rb-in" type="text" placeholder="Recipient name" aria-label="Recipient name">
</div> </section>
</main>@use "sass:color";
@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
$line: color.mix($subject-ink, $stage-paper, 18%);
$soft: color.mix($subject-ink, $stage-paper, 6%);
$mute: rgba($subject-ink, .72);
.rb {
box-sizing: border-box;
width: min(312px, 92vw);
padding: $sp-3;
border: 1px solid $line;
border-radius: $r-card;
background: $stage-paper;
color: $subject-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
// ── 위에서 내려오는 이어쓰기 띠 ─────────────────────
// 띠 자체는 루프에서 사라지지 않는다(지운 뒤 빈 화면이 대표 이미지가 된다)
.rb__bar {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2;
border-radius: $r-control;
background: $soft;
}
.rb__bar-i { display: inline-flex; flex: 0 0 auto; width: 18px; height: 18px; color: $color; }
.rb__bar-t {
flex: 1 1 auto;
margin: 0;
color: $subject-ink;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
}
.rb__bar-a {
display: inline-flex;
gap: $sp-1;
}
.rb__b {
margin: 0;
padding: $sp-1 $sp-2;
border: 1px solid $line;
border-radius: $r-pill;
background: transparent;
color: $mute;
font-family: inherit;
font-size: 10px;
font-weight: 700;
line-height: 1.3;
white-space: nowrap;
cursor: pointer;
}
.rb__b--go { border-color: $subject-blue-deep; background: $subject-blue-deep; color: $subject-cream; }
.rb__b:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 아래 폼 ───────────────────────────────────────
.rb__form { padding-top: $sp-3; }
.rb__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: $sp-2;
}.rb__h {
margin: 0;
color: $mute;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
}
.rb__now { position: relative; display: inline-block; width: 12px; }
.rb__d { position: absolute; inset: 0; color: $subject-ink; font-size: 11px; }
.rb__d[data-d="3"] { visibility: hidden; }
// 정적(사람이 조작하는) 경로 — 이어서를 누르면 3단계 표시가 켜진다
.rb.is-at3 .rb__d[data-d="1"] { visibility: hidden; }
.rb.is-at3 .rb__d[data-d="3"] { visibility: visible; }
.rb__save {
margin: 0;
color: $mute;
font-size: 10px;
line-height: 1.3;
opacity: 0;
}
// 칸을 칠 때마다 저장 표시가 떴다 가라앉는다
.rb.is-on .rb__save { opacity: 1; }
.rb.is-fresh .rb__save { opacity: 0; }
.rb__dots {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $sp-1;
margin-top: $sp-2;
}
.rb__dot {
height: 4px;
border-radius: $r-pill;
background: $soft;
overflow: hidden;
}
.rb__dot-f {
display: block;
height: 100%;
border-radius: inherit;
background: $color;
transform: scaleX(0);
transform-origin: left center; // 규칙8: 단계 점 좌측에서 채움
transition: transform $duration $easing;
}
// 정적 경로 — 이어서를 누른 단계까지의 점이 차 있다
.rb.is-at3 .rb__dot[data-dot="1"] .rb__dot-f,
.rb.is-at3 .rb__dot[data-dot="2"] .rb__dot-f,
.rb.is-at3 .rb__dot[data-dot="3"] .rb__dot-f { transform: scaleX(1); }
.rb__in {
box-sizing: border-box;
width: 100%;
height: 24px;
margin: $sp-2 0 0;
padding: 0 $sp-2;
border: 1px solid $line;
border-radius: $r-sm;
background: $stage-paper;
color: $subject-ink;
font-family: inherit;
font-size: 11px;
line-height: 1.3;
}
.rb__in:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 격자 미리보기 전용 2초 루프 ─────────────────────
// 띠는 내려와 자리 잡고 그대로 남는다(퇴장 재현 금지).
// 띠 아래 폼의 단계 표시가 1 → 3 으로 컷 점프하고 점이 따라 찬다
@keyframes rb-drop {
0% { transform: translateY(-30px); opacity: 0; }
14%, 100% { transform: translateY(0); opacity: 1; }
}
@keyframes rb-ico {
0%, 5% { transform: scale(1); }
11% { transform: scale(1.2); }
18%, 100% { transform: scale(1); }
}
@keyframes rb-d3 {
0%, 44% { visibility: hidden; }
45%, 100% { visibility: visible; }
}
@keyframes rb-d1 {
0%, 44% { visibility: visible; }
45%, 100% { visibility: hidden; }
}
@keyframes rb-dot3 {
0%, 44% { transform: scaleX(0); }
52%, 100% { transform: scaleX(1); }
}
@keyframes rb-in-pop {
0%, 44% { transform: translateY(0); }
50% { transform: translateY(2px); }
58%, 100% { transform: translateY(0); }
}
.rb.is-demo {
.rb__bar {
animation-name: rb-drop;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.rb__bar-i {
animation-name: rb-ico;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.rb__d[data-d="1"] { animation-name: rb-d1; }
.rb__d[data-d="3"] { animation-name: rb-d3; }
.rb__d {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.rb__dot[data-dot="3"] .rb__dot-f { animation-name: rb-dot3; }
.rb__dot[data-dot="3"] .rb__dot-f {
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.rb__in {
animation-name: rb-in-pop;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
}
@media (max-width: 340px) {
.rb { padding: $sp-2; }
.rb__bar { gap: $sp-1; padding: $sp-1; }
.rb__b { padding: 0 $sp-1; }
.rb__bar-t { font-size: 10px; }
.rb__form { padding-top: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.rb.is-demo .rb__bar,
.rb.is-demo .rb__bar-i,
.rb.is-demo .rb__d,
.rb.is-demo .rb__dot-f,
.rb.is-demo .rb__in { animation: none !important; transform: none !important; }
// 움직임만 끄고 내려온 띠와 3단계 판정은 남긴다
.rb.is-demo .rb__bar { opacity: 1 !important; }
.rb.is-demo .rb__d[data-d="1"] { visibility: hidden !important; }
.rb.is-demo .rb__d[data-d="3"] { visibility: visible !important; }
.rb.is-demo .rb__dot[data-dot="3"] .rb__dot-f { transform: scaleX(1) !important; }
}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);
}
.rb {
box-sizing: border-box;
width: min(312px, 92vw);
padding: 12px;
border: 1px solid rgb(83.6235294118%, 83.4117647059%, 83.8352941176%);
border-radius: 16px;
background: #ffffff;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.rb__bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 12px;
background: rgb(94.5411764706%, 94.4705882353%, 94.6117647059%);
}
.rb__bar-i {
display: inline-flex;
flex: 0 0 auto;
width: 18px;
height: 18px;
color: #2f6df6;
}
.rb__bar-t {
flex: 1 1 auto;
margin: 0;
color: #17141a;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
}
.rb__bar-a {
display: inline-flex;
gap: 4px;
}
.rb__b {
margin: 0;
padding: 4px 8px;
border: 1px solid rgb(83.6235294118%, 83.4117647059%, 83.8352941176%);
border-radius: 999px;
background: transparent;
color: rgba(23, 20, 26, 0.72);
font-family: inherit;
font-size: 10px;
font-weight: 700;
line-height: 1.3;
white-space: nowrap;
cursor: pointer;
}
.rb__b--go {
border-color: #0a4fe8;
background: #0a4fe8;
color: #fff7e6;
}
.rb__b:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.rb__form {
padding-top: 12px;
}
.rb__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.rb__h {
margin: 0;
color: rgba(23, 20, 26, 0.72);
font-size: 11px;
font-weight: 700;
line-height: 1.3;
}
.rb__now {
position: relative;
display: inline-block;
width: 12px;
}
.rb__d {
position: absolute;
inset: 0;
color: #17141a;
font-size: 11px;
}
.rb__d[data-d="3"] {
visibility: hidden;
}
.rb.is-at3 .rb__d[data-d="1"] {
visibility: hidden;
}
.rb.is-at3 .rb__d[data-d="3"] {
visibility: visible;
}
.rb__save {
margin: 0;
color: rgba(23, 20, 26, 0.72);
font-size: 10px;
line-height: 1.3;
opacity: 0;
}
.rb.is-on .rb__save {
opacity: 1;
}
.rb.is-fresh .rb__save {
opacity: 0;
}
.rb__dots {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
margin-top: 8px;
}
.rb__dot {
height: 4px;
border-radius: 999px;
background: rgb(94.5411764706%, 94.4705882353%, 94.6117647059%);
overflow: hidden;
}
.rb__dot-f {
display: block;
height: 100%;
border-radius: inherit;
background: #2f6df6;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rb.is-at3 .rb__dot[data-dot="1"] .rb__dot-f,
.rb.is-at3 .rb__dot[data-dot="2"] .rb__dot-f,
.rb.is-at3 .rb__dot[data-dot="3"] .rb__dot-f {
transform: scaleX(1);
}
.rb__in {
box-sizing: border-box;
width: 100%;
height: 24px;
margin: 8px 0 0;
padding: 0 8px;
border: 1px solid rgb(83.6235294118%, 83.4117647059%, 83.8352941176%);
border-radius: 8px;
background: #ffffff;
color: #17141a;
font-family: inherit;
font-size: 11px;
line-height: 1.3;
}
.rb__in:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
@keyframes rb-drop {
0% {
transform: translateY(-30px);
opacity: 0;
}
14%, 100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes rb-ico {
0%, 5% {
transform: scale(1);
}
11% {
transform: scale(1.2);
}
18%, 100% {
transform: scale(1);
}
}
@keyframes rb-d3 {
0%, 44% {
visibility: hidden;
}
45%, 100% {
visibility: visible;
}
}
@keyframes rb-d1 {
0%, 44% {
visibility: visible;
}
45%, 100% {
visibility: hidden;
}
}
@keyframes rb-dot3 {
0%, 44% {
transform: scaleX(0);
}
52%, 100% {
transform: scaleX(1);
}
}
@keyframes rb-in-pop {
0%, 44% {
transform: translateY(0);
}
50% {
transform: translateY(2px);
}
58%, 100% {
transform: translateY(0);
}
}
.rb.is-demo .rb__bar {
animation-name: rb-drop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.rb.is-demo .rb__bar-i {
animation-name: rb-ico;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.rb.is-demo .rb__d[data-d="1"] {
animation-name: rb-d1;
}
.rb.is-demo .rb__d[data-d="3"] {
animation-name: rb-d3;
}
.rb.is-demo .rb__d {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.rb.is-demo .rb__dot[data-dot="3"] .rb__dot-f {
animation-name: rb-dot3;
}
.rb.is-demo .rb__dot[data-dot="3"] .rb__dot-f {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.rb.is-demo .rb__in {
animation-name: rb-in-pop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.rb {
padding: 8px;
}
.rb__bar {
gap: 4px;
padding: 4px;
}
.rb__b {
padding: 0 4px;
}
.rb__bar-t {
font-size: 10px;
}
.rb__form {
padding-top: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.rb.is-demo .rb__bar,
.rb.is-demo .rb__bar-i,
.rb.is-demo .rb__d,
.rb.is-demo .rb__dot-f,
.rb.is-demo .rb__in {
animation: none !important;
transform: none !important;
}
.rb.is-demo .rb__bar {
opacity: 1 !important;
}
.rb.is-demo .rb__d[data-d="1"] {
visibility: hidden !important;
}
.rb.is-demo .rb__d[data-d="3"] {
visibility: visible !important;
}
.rb.is-demo .rb__dot[data-dot="3"] .rb__dot-f {
transform: scaleX(1) !important;
}
}// 실제 동작: 칸을 칠 때마다 단계 번호와 값을 localStorage 에 적고,
// 열 때 저장된 게 있으면 그 단계로 데려다 준다. iframe 이 저장소를 막을 수 있어 try/catch.
var rb = document.getElementById('rb');
var rbIn = document.getElementById('rb-in');
var rbGo = document.getElementById('rb-resume');
var rbMark = document.getElementById('rb-save');
var RB_KEY = 'godrich-265-04';
var rbStep = 1;
function rbRead() {
try { return JSON.parse(localStorage.getItem(RB_KEY) || 'null'); } catch (e) { return null; }
}
function rbWrite(o) {
try { localStorage.setItem(RB_KEY, JSON.stringify(o)); } catch (e) {}
}
function rbAt(n) { rbStep = n; rb.classList.toggle('is-at3', n >= 3); }
var rbDraft = rbRead();
if (!rbDraft) {
// 데모라 직전 세션이 없다 — 이어쓸 거리가 있도록 예시 초안을 한 번 적어 둔다
rbDraft = { step: 3, v: '' };
rbWrite(rbDraft);
}
rbGo.textContent = rbGo.getAttribute('data-t').replace('{n}', rbDraft.step);
rbIn.addEventListener('input', function () {
rb.classList.remove('is-demo');
rbWrite({ step: rbStep, v: rbIn.value });
rbMark.classList.add('is-on');
});
rbGo.addEventListener('click', function () {
rb.classList.remove('is-demo');
var d = rbRead();
rbAt(d ? d.step : 1);
rbIn.value = d ? d.v : '';
rbIn.focus({ preventScroll: true });
});
document.getElementById('rb-fresh').addEventListener('click', function () {
rb.classList.remove('is-demo');
rbWrite({ step: 1, v: '' });
rb.classList.add('is-fresh');
rbAt(1);
rbIn.value = '';
rbIn.focus({ preventScroll: true });
});
rbAt(1);import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ResumeDraftBanner.module.scss";
export type ResumeDraftBannerProps = {
bannerText?: string;
resumePrefix?: string;
resumeSuffix?: string;
freshLabel?: string;
headingPrefix?: string;
headingSuffix?: string;
fieldLabel?: string;
fieldPlaceholder?: string;
savedLabel?: string;
groupLabel?: string;
totalSteps?: number;
/** 데모라 직전 세션이 없을 때 이어쓸 초안 단계 */
seedStep?: number;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type KeyEvt = { key: string; preventDefault(): void };
type InputEvt = { target: { value: string } };
type Box<T> = { current: T };
const STORAGE_KEY = "godrich-265-04-react";
type Draft = { step: number; v: string };
function readDraft(seedStep: number): Draft {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const d = JSON.parse(raw) as Draft;
if (typeof d.step === "number") return { step: d.step, v: String(d.v || "") };
}
} catch {
// iframe 이 저장소를 막을 수 있다 — 없을 때와 똑같이 논다
}
return { step: seedStep, v: "" };
}
function writeDraft(d: Draft) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(d));
} catch {
// 저장 실패는 화면을 깨지 않는다
}
}
function ClockBack() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2" />
<path d="M136 80v43.47l36.12 21.67a8 8 0 0 1-8.24 13.72l-40-24A8 8 0 0 1 120 128V80a8 8 0 0 1 16 0m-8-48a95.44 95.44 0 0 0-67.92 28.15C52.81 67.51 46.35 74.59 40 82V64a8 8 0 0 0-16 0v40a8 8 0 0 0 8 8h40a8 8 0 0 0 0-16H49c7.15-8.42 14.27-16.35 22.39-24.57a80 80 0 1 1 1.66 114.75a8 8 0 1 0-11 11.64A96 96 0 1 0 128 32" />
</g>
</svg>
);
}
export default function ResumeDraftBanner({
bannerText = "You have an unfinished draft",
resumePrefix = "Resume from step ",
resumeSuffix = "",
freshLabel = "Start over",
headingPrefix = "/ 4 · Application",
headingSuffix = "",
fieldLabel = "Recipient name",
fieldPlaceholder = "Recipient name",
savedLabel = "Saved automatically",
groupLabel = "Application that remembers where you stopped",
totalSteps = 4,
seedStep = 3,
demo = true,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: ResumeDraftBannerProps) {
const [draft, setDraft] = useState<Draft>({ step: 1, v: "" });
const [step, setStep] = useState(1);
const [live, setLive] = useState(demo);
const [saved, setSaved] = useState(false);
const input = useRef<HTMLInputElement>(null) as Box<HTMLInputElement | null>;
// 열 때 저장된 초안을 읽어 띠 문구를 맞춘다
useEffect(() => {
const d = readDraft(seedStep);
setDraft(d);
if (!localStorage.getItem(STORAGE_KEY)) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(d));
} catch {
/* 저장소가 막혀도 띠는 seedStep 로 보여 준다 */
}
}
}, [seedStep]);
function onInput(e: InputEvt) {
setLive(false);
setSaved(true);
const next = { step, v: e.target.value };
setDraft(next);
writeDraft(next);
}
function resume() {
setLive(false);
const d = readDraft(seedStep);
setStep(d.step);
setDraft(d);
if (input.current) {
input.current.value = d.v;
input.current.focus({ preventScroll: true });
}
}
function fresh() {
setLive(false);
const next = { step: 1, v: "" };
setStep(1);
setDraft(next);
setSaved(false);
writeDraft(next);
if (input.current) {
input.current.value = "";
input.current.focus({ preventScroll: true });
}
}
function onKey(e: KeyEvt) {
if (e.key === "Enter") {
e.preventDefault();
resume();
}
}
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const cls = [styles.rb, live ? styles.isDemo : "", step >= 3 ? styles.isAt3 : "", saved ? styles.isOn : ""]
.filter(Boolean)
.join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<div className={styles.bar} role="status" aria-live="polite">
<span className={styles.barI} aria-hidden="true"><ClockBack /></span>
<p className={styles.barT}>{bannerText}</p>
<span className={styles.barA}>
<button className={[styles.b, styles.bGo].join(" ")} type="button" onClick={resume}>
{resumePrefix + draft.step + resumeSuffix}
</button>
<button className={styles.b} type="button" onClick={fresh}>
{freshLabel}
</button>
</span>
</div>
<div className={styles.form}>
<div className={styles.head}>
<p className={styles.h}>
<span className={styles.now}>
<b className={styles.d} data-d="1">1</b>
<b className={styles.d} data-d="3">3</b>
</span>
{" "}{headingPrefix}{headingSuffix}
</p>
<p className={styles.save}>{savedLabel}</p>
</div>
<div className={styles.dots} aria-hidden="true">
{Array.from({ length: totalSteps }, (_, i) => (
<span key={i} className={styles.dot} data-dot={i + 1}>
<span className={styles.dotF} />
</span>
))}
</div>
<input
ref={input}
className={styles.in}
type="text"
placeholder={fieldPlaceholder}
aria-label={fieldLabel}
onInput={onInput}
onKeyDown={onKey}
/>
</div>
</section>
);
}04 Resume strip that remembers where you stopped — Measured area 10.174% · moving frames 7/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
Inputs07 Arrival chip whose date slides over when you change the shipping speedCheckout Page Design — 9 Cart Patterns, No Library
Inputs03 Inline hint that flips while you type9 Form Validation UI Patterns — As You Type
Inputs04 Recent-search chips — recomputing stored timestamps into '3 min ago, 2 days ago' labelsAutocomplete Input: 9 Search Dropdowns