<div><span class="lbl">Steps expand in place</span>
<form class="wz" data-wizard aria-labelledby="mf-title">
<h2 class="wz-title" id="mf-title">Open your shop on Marketfold</h2>
<p class="wz-sub">Four steps, about three minutes. Nothing goes live until you open the shop.</p>
<ol class="wz-vsteps" data-stepper aria-label="Progress">
<li>
<span class="wz-dot" aria-hidden="true"><b>1</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h1" tabindex="-1"><span class="wz-kick">Step 1 of 4</span>Store details<span class="sr-only" data-sr></span></h3>
<p class="wz-sum"><span data-out="shop">Marketfold Ceramics</span> · <span data-out="currency">USD</span></p>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h1">
<div class="wz-row">
<div class="wz-field"><label for="mf-shop">Shop name</label>
<input class="wz-input" id="mf-shop" name="shop" required autocomplete="organization" value="Marketfold Ceramics"></div>
<div class="wz-field"><label for="mf-currency">Currency</label>
<select class="wz-input" id="mf-currency" name="currency"><option value="USD">US dollar (USD)</option><option value="EUR">Euro (EUR)</option><option value="GBP">Pound sterling (GBP)</option><option value="CAD">Canadian dollar (CAD)</option></select></div>
</div>
<div class="wz-field"><label for="mf-handle">Shop address</label>
<div class="wz-affix"><span>marketfold.com/</span><input class="wz-input" id="mf-handle" name="handle" required pattern="[a-z0-9\-]+" aria-describedby="mf-handle-hint" value="marketfold-ceramics"></div>
<span class="wz-hint" id="mf-handle-hint">Lowercase letters, numbers and hyphens.</span></div>
<div class="wz-actions">
<button type="button" class="wz-btn" data-next hidden>Continue</button>
</div>
</section>
</li>
<li>
<span class="wz-dot" aria-hidden="true"><b>2</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h2" tabindex="-1"><span class="wz-kick">Step 2 of 4</span>Shipping<span class="sr-only" data-sr></span></h3>
<p class="wz-sum">Ships from <span data-out="from">Portugal</span> · <span data-out="processing">3–5 days</span></p>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h2">
<div class="wz-field"><label for="mf-from">Ships from</label>
<select class="wz-input" id="mf-from" name="from" required><option>Portugal</option><option>Spain</option><option>United States</option><option>Canada</option><option>United Kingdom</option></select></div>
<fieldset class="wz-field"><legend class="wz-legend">Processing time</legend>
<div class="wz-seg">
<label><input type="radio" name="processing" value="1–2 days" required>1–2 days</label>
<label><input type="radio" name="processing" value="3–5 days" checked>3–5 days</label>
<label><input type="radio" name="processing" value="1–2 weeks">1–2 weeks</label>
</div></fieldset>
<div class="wz-actions">
<button type="button" class="wz-btn" data-next hidden>Continue</button>
<button type="button" class="wz-btn ghost" data-back hidden>Back</button>
</div>
</section>
</li>
<li>
<span class="wz-dot" aria-hidden="true"><b>3</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h3" tabindex="-1"><span class="wz-kick">Step 3 of 4</span>Payments<span class="sr-only" data-sr></span></h3>
<p class="wz-sum"><span data-out="payout">PayPal</span> · <span data-out="account">[email protected]</span></p>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h3">
<fieldset class="wz-field"><legend class="wz-legend">Payout method</legend>
<div class="wz-tiles">
<label class="wz-tile"><input type="radio" name="payout" value="Bank transfer" required><b>Bank transfer</b><span>Every Friday · no fee</span></label>
<label class="wz-tile"><input type="radio" name="payout" value="PayPal" checked><b>PayPal</b><span>Next working day · 1% fee</span></label>
</div></fieldset>
<div class="wz-field"><label for="mf-account">IBAN or PayPal email</label>
<input class="wz-input" id="mf-account" name="account" required aria-describedby="mf-account-hint" value="[email protected]">
<span class="wz-hint" id="mf-account-hint">Bank transfer: your IBAN. PayPal: the email on your PayPal account.</span></div>
<div class="wz-actions">
<button type="button" class="wz-btn" data-next hidden>Continue</button>
<button type="button" class="wz-btn ghost" data-back hidden>Back</button>
</div>
</section>
</li>
<li>
<span class="wz-dot" aria-hidden="true"><b>4</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h4" tabindex="-1"><span class="wz-kick">Step 4 of 4</span>Launch<span class="sr-only" data-sr></span></h3>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h4">
<p class="wz-url"><span>Your shop will open at</span><b>marketfold.com/<span data-out="handle">marketfold-ceramics</span></b></p>
<p class="wz-lead" style="margin:0">Buyers can find you as soon as you open. You can pause the shop from Settings at any time.</p>
<div class="wz-actions">
<button type="submit" class="wz-btn">Open shop</button>
<button type="button" class="wz-btn ghost" data-back hidden>Back</button>
</div>
</section>
</li>
</ol>
<section class="wz-panel wz-done" data-done hidden aria-labelledby="mf-h5">
<span class="wz-dot"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<h3 class="wz-h" id="mf-h5" tabindex="-1"><span data-out="shop">Marketfold Ceramics</span> is open</h3>
<p class="wz-lead" style="margin:0">Add your first listing and it appears at marketfold.com/<span data-out="handle">marketfold-ceramics</span>.</p>
<button type="button" class="wz-btn ghost" data-restart>Set up another shop</button>
</section>
</form></div>
<div><span class="lbl">Side rail with descriptions</span>
<form class="wz wz-split" data-wizard aria-labelledby="gr-title">
<div class="wz-rail">
<h2 class="wz-title" id="gr-title">Grant application</h2>
<p class="wz-sub">Riverside Arts Council · Spring 2027 round</p>
<ol class="wz-rsteps" data-stepper aria-label="Progress">
<li><span class="wz-dot"><b>1</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Applicant</span><span class="wz-desc">Who is applying and how to reach you</span></span><span class="sr-only" data-sr></span></li>
<li><span class="wz-dot"><b>2</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Project</span><span class="wz-desc">What you will make and when</span></span><span class="sr-only" data-sr></span></li>
<li><span class="wz-dot"><b>3</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Budget</span><span class="wz-desc">The amount you ask for</span></span><span class="sr-only" data-sr></span></li>
<li><span class="wz-dot"><b>4</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Attachments</span><span class="wz-desc">Samples and sign-off</span></span><span class="sr-only" data-sr></span></li>
</ol>
</div>
<div class="wz-main">
<section class="wz-panel" data-step aria-labelledby="gr-h1">
<h3 class="wz-h" id="gr-h1" tabindex="-1"><span class="wz-kick">Step 1 of 4</span>About the applicant</h3>
<p class="wz-lead">Individuals and groups based in Riverside County can apply.</p>
<div class="wz-row">
<div class="wz-field"><label for="gr-name">Full name</label>
<input class="wz-input" id="gr-name" name="name" required autocomplete="name" value="Amara Okafor"></div>
<div class="wz-field"><label for="gr-org">Organisation</label>
<input class="wz-input" id="gr-org" name="organisation" autocomplete="organization" aria-describedby="gr-org-hint" value="Tidewater Print Collective">
<span class="wz-hint" id="gr-org-hint">Leave empty if you apply as an individual.</span></div>
</div>
<div class="wz-field"><label for="gr-email">Email</label>
<input class="wz-input" id="gr-email" name="email" type="email" required autocomplete="email" value="[email protected]"></div>
</section>
<section class="wz-panel" data-step aria-labelledby="gr-h2">
<h3 class="wz-h" id="gr-h2" tabindex="-1"><span class="wz-kick">Step 2 of 4</span>Your project</h3>
<div class="wz-field"><label for="gr-project">Project title</label>
<input class="wz-input" id="gr-project" name="project" required value="Riverside Linocut Trail"></div>
<div class="wz-field"><label for="gr-summary">Summary</label>
<textarea class="wz-input" id="gr-summary" name="summary" required minlength="80" aria-describedby="gr-summary-hint">Twelve large linocut panels printed with residents of the Eastbank estate and mounted along the river path between Mill Bridge and the ferry landing.</textarea>
<span class="wz-hint" id="gr-summary-hint">At least 80 characters: what you will make, where, and with whom.</span></div>
<div class="wz-field" style="max-width:240px"><label for="gr-start">Start month</label>
<input class="wz-input" id="gr-start" name="start" type="month" required min="2027-01" value="2027-03"></div>
</section>
<section class="wz-panel" data-step aria-labelledby="gr-h3">
<h3 class="wz-h" id="gr-h3" tabindex="-1"><span class="wz-kick">Step 3 of 4</span>Budget</h3>
<div class="wz-row">
<div class="wz-field"><label for="gr-amount">Amount requested</label>
<div class="wz-affix"><span>$</span><input class="wz-input" id="gr-amount" name="amount" type="number" required min="500" max="15000" step="50" aria-describedby="gr-amount-hint" value="9800"></div>
<span class="wz-hint" id="gr-amount-hint">Between $500 and $15,000.</span></div>
<div class="wz-field"><label for="gr-other">Other funding</label>
<select class="wz-input" id="gr-other" name="other"><option>None</option><option>Applied, awaiting a decision</option><option>Confirmed</option></select></div>
</div>
</section>
<section class="wz-panel" data-step aria-labelledby="gr-h4">
<h3 class="wz-h" id="gr-h4" tabindex="-1"><span class="wz-kick">Step 4 of 4</span>Attachments</h3>
<div class="wz-field"><label for="gr-files">Work samples (optional)</label>
<input class="wz-input" id="gr-files" name="samples" type="file" multiple accept=".pdf,.jpg,.jpeg,application/pdf,image/jpeg" aria-describedby="gr-files-hint">
<span class="wz-hint" id="gr-files-hint">PDF or JPG. Photos of earlier work help the panel.</span></div>
<label class="wz-check"><input type="checkbox" name="confirm" required>I confirm the information is accurate</label>
</section>
<section class="wz-panel wz-done" data-done hidden aria-labelledby="gr-h5">
<span class="wz-dot"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<h3 class="wz-h" id="gr-h5" tabindex="-1">Application received</h3>
<p class="wz-ref">Reference <b>RAC-2026-0417</b></p>
<p class="wz-lead" style="margin:0">We sent a copy to <span data-out="email">[email protected]</span>. The council announces spring awards on 15 January 2027.</p>
<button type="button" class="wz-btn ghost" data-restart>Start a new application</button>
</section>
<div class="wz-nav" data-nav>
<button type="button" class="wz-btn ghost" data-back hidden>Back</button>
<button type="button" class="wz-btn" data-next hidden>Continue</button>
<button type="submit" class="wz-btn">Submit application</button>
</div>
</div>
</form></div>
CSS
/* Vertical wizard: an <ol> stepper with one <li> per step, one <section data-step> per step and Back / Continue buttons.
The script hides every step but the current one, sets data-state and aria-current="step" on the stepper
and moves focus to the new step's heading. Without JavaScript every step shows and the form submits normally. */
.wz{--wz-accent:#047857;--wz-accent-dark:#065f46;--wz-soft:#d1fae5;--wz-tint:#ecfdf5;--wz-ink:#14201a;--wz-muted:#56615a;--wz-line:#808c84;--wz-field:#78847c;--wz-rail:#f6f8f4;
background:#fff;border-radius:18px;padding:30px 32px;box-shadow:0 1px 2px rgba(20,32,26,.06),0 12px 32px -14px rgba(6,78,59,.22);color:var(--wz-ink)}
.wz [hidden]{display:none!important}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.wz-title{font-size:1.3rem;font-weight:700;letter-spacing:-.015em;margin:0}
.wz-sub{margin:4px 0 0;color:var(--wz-muted);font-size:.92rem}
.wz-dot{flex:none;width:36px;height:36px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--wz-line);background:#fff;font-weight:700;font-size:.9rem;color:#4a554e;transition:background .25s,border-color .25s,box-shadow .25s}
.wz-dot svg{display:none;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.wz [data-state="current"] > .wz-dot{border-color:var(--wz-accent);color:var(--wz-accent-dark);box-shadow:0 0 0 5px var(--wz-soft)}
.wz [data-state="done"] > .wz-dot{border-color:var(--wz-accent);background:var(--wz-accent);color:#fff}
.wz [data-state="done"] > .wz-dot b{display:none}
.wz [data-state="done"] > .wz-dot svg{display:block}
/* expand in place: each <li> is dot + header + the step's section; a line joins the dots and fills when a step is done */
.wz-vsteps{list-style:none;margin:26px 0 0;padding:0}
.wz-vsteps > li{position:relative;display:grid;grid-template-columns:36px minmax(0,1fr);column-gap:16px;padding-bottom:22px}
.wz-vsteps > li:last-child{padding-bottom:0}
.wz-vsteps > li:not(:last-child)::before{content:"";position:absolute;left:17px;top:46px;bottom:8px;width:2px;border-radius:2px;background:var(--wz-line);transition:background .25s}
.wz-vsteps > li[data-state="done"]::before{background:var(--wz-accent)}
.wz-vsteps .wz-dot{margin-top:4px}
.wz-head{min-width:0}
.wz-head .wz-h{font-size:1.02rem}
.wz-vsteps [data-state="next"] .wz-h,.wz-vsteps [data-state="next"] .wz-kick{color:var(--wz-muted)}
.wz-vsteps [data-state="next"] .wz-h{font-weight:600}
.wz-sum{display:none;margin:2px 0 0;font-size:.86rem;color:var(--wz-muted);overflow-wrap:anywhere}
.wz-vsteps [data-state="done"] .wz-sum{display:block}
.wz-body{grid-column:2;display:grid;gap:16px;padding-top:16px;animation:wz-in .3s ease}
.wz-actions{display:flex;flex-wrap:wrap;gap:10px;padding-top:4px}
.wz-url{display:grid;gap:4px;margin:0;padding:16px 18px;border-radius:12px;background:var(--wz-tint);border:1px solid #a7e3c9}
.wz-url > span{font-size:.8rem;color:var(--wz-muted)}
.wz-url b{font-size:1.02rem;color:var(--wz-accent-dark);overflow-wrap:anywhere}
/* side rail: steps with descriptions on the left, the current step on the right */
.wz-split{display:grid;grid-template-columns:250px minmax(0,1fr);padding:0;overflow:hidden}
.wz-rail{background:var(--wz-rail);border-right:1px solid #e1e7df;padding:30px 24px}
.wz-main{padding:30px 32px;display:flex;flex-direction:column;min-width:0}
.wz-rsteps{list-style:none;margin:28px 0 0;padding:0}
.wz-rsteps li{position:relative;display:flex;gap:14px;padding-bottom:26px;color:var(--wz-muted)}
.wz-rsteps li:last-child{padding-bottom:0}
.wz-rsteps li:not(:last-child)::before{content:"";position:absolute;left:17px;top:42px;bottom:6px;width:2px;border-radius:2px;background:var(--wz-line);transition:background .25s}
.wz-rsteps li[data-state="done"]::before{background:var(--wz-accent)}
.wz-rtext{display:grid;gap:1px;padding-top:6px;min-width:0}
.wz-name{font-size:.92rem;font-weight:600}
.wz-desc{font-size:.8rem;line-height:1.4}
.wz-rsteps [data-state="current"],.wz-rsteps [data-state="done"]{color:var(--wz-ink)}
.wz-rsteps [data-state="done"] .wz-desc,.wz-rsteps [data-state="current"] .wz-desc{color:var(--wz-muted)}
@media (max-width:640px){
.wz-split{grid-template-columns:1fr}
.wz-rail{border-right:0;border-bottom:1px solid #e1e7df;padding:24px 18px 18px}
.wz-main{padding:24px 18px}
.wz-rsteps{display:flex;align-items:center;gap:8px;margin-top:18px}
.wz-rsteps li{padding:0;align-items:center;gap:10px}
.wz-rsteps li::before,.wz-desc{display:none}
.wz-rsteps .wz-dot{width:30px;height:30px;font-size:.8rem}
.wz-rtext{padding:0}
.wz-rsteps [data-state]:not([data-state="current"]) .wz-rtext{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}}
/* step panels */
.wz-panel{display:grid;gap:18px;animation:wz-in .3s ease}
@keyframes wz-in{from{opacity:0;transform:translateY(6px)}}
.wz-h{margin:0;font-size:1.15rem;font-weight:700;letter-spacing:-.01em}
.wz-h:focus{outline:none}
.wz-kick{display:block;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--wz-accent-dark);margin-bottom:2px}
.wz-lead{margin:-10px 0 0;color:var(--wz-muted);font-size:.92rem}
.wz-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:16px}
.wz-field{display:grid;gap:6px;min-width:0;align-content:start}
.wz-field > label,.wz-legend{font-size:.88rem;font-weight:600}
.wz-hint{font-size:.8rem;color:var(--wz-muted)}
.wz-input{width:100%;font:inherit;font-size:.95rem;color:var(--wz-ink);background:#fff;border:1.5px solid var(--wz-field);border-radius:10px;padding:10px 12px;min-height:44px}
textarea.wz-input{min-height:108px;resize:vertical}
.wz-input:focus-visible{outline:none;border-color:var(--wz-accent);box-shadow:0 0 0 4px var(--wz-soft)}
.wz-input[type="file"]{padding:6px}
.wz-input::file-selector-button{font:inherit;font-size:.88rem;font-weight:600;margin-right:12px;padding:7px 14px;border:0;border-radius:7px;background:var(--wz-soft);color:var(--wz-accent-dark);cursor:pointer}
.wz-affix{display:flex;align-items:stretch;border:1.5px solid var(--wz-field);border-radius:10px;overflow:hidden}
.wz-affix span{display:flex;align-items:center;padding:0 12px;background:var(--wz-rail);color:#4a554e;font-size:.9rem;border-right:1.5px solid var(--wz-field)}
.wz-affix .wz-input{border:0;border-radius:0;min-width:0}
.wz-affix:has(:focus-visible){border-color:var(--wz-accent);box-shadow:0 0 0 4px var(--wz-soft)}
.wz-affix .wz-input:focus-visible{box-shadow:none}
fieldset.wz-field{border:0;margin:0;padding:0}
.wz-legend{padding:0;margin-bottom:6px}
/* choice pills and tiles: a radio inside each label, the outline follows :has(:checked) */
.wz-seg{display:flex;flex-wrap:wrap;gap:8px}
.wz-seg label{display:flex;align-items:center;gap:8px;padding:9px 14px;border:1.5px solid var(--wz-field);border-radius:999px;font-size:.9rem;cursor:pointer;transition:border-color .2s,background .2s}
.wz-seg input{width:16px;height:16px;margin:0;accent-color:var(--wz-accent)}
.wz-seg label:has(:checked){border-color:var(--wz-accent);background:var(--wz-tint);color:var(--wz-accent-dark);font-weight:600}
.wz-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.wz-tile{position:relative;display:grid;gap:2px;padding:14px 14px 14px 44px;border:1.5px solid var(--wz-field);border-radius:12px;cursor:pointer;transition:border-color .2s,background .2s}
.wz-tile input{position:absolute;left:14px;top:16px;width:18px;height:18px;margin:0;accent-color:var(--wz-accent)}
.wz-tile b{font-size:.95rem}
.wz-tile span{font-size:.82rem;color:var(--wz-muted)}
.wz-tile:has(:checked){border-color:var(--wz-accent);background:var(--wz-tint)}
.wz-check{display:flex;align-items:flex-start;gap:10px;font-size:.92rem;cursor:pointer}
.wz-check input{flex:none;width:18px;height:18px;margin:2px 0 0;accent-color:var(--wz-accent)}
.wz-seg input:focus-visible,.wz-tile input:focus-visible,.wz-check input:focus-visible{outline:2px solid var(--wz-accent);outline-offset:3px}
/* buttons */
.wz-nav{display:flex;gap:10px;margin-top:auto;padding-top:20px;border-top:1px solid #e6ebe4}
.wz-main > .wz-panel{margin-bottom:28px}
.wz-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 22px;border-radius:10px;border:1.5px solid var(--wz-accent);background:var(--wz-accent);color:#fff;font:inherit;font-size:.95rem;font-weight:600;cursor:pointer;transition:background .2s,box-shadow .2s}
.wz-btn:hover{background:var(--wz-accent-dark)}
.wz-btn:focus-visible{outline:none;box-shadow:0 0 0 3px #fff,0 0 0 5px var(--wz-accent)}
.wz-btn.ghost{background:#fff;color:var(--wz-accent-dark);border-color:var(--wz-line)}
.wz-btn.ghost:hover{background:var(--wz-tint)}
.wz-nav .wz-btn:not(.ghost){margin-left:auto}
.wz-done{text-align:center;justify-items:center;padding:12px 0 4px}
.wz-vsteps + .wz-done{margin-top:26px;padding-top:26px;border-top:1px solid #e6ebe4}
.wz-done .wz-dot{width:56px;height:56px;border:0;background:var(--wz-soft);color:var(--wz-accent-dark)}
.wz-done .wz-dot svg{display:block;width:26px;height:26px}
.wz-ref{margin:0;padding:8px 16px;border-radius:999px;background:var(--wz-tint);border:1px dashed var(--wz-accent);font-size:.9rem;color:var(--wz-accent-dark)}
@media (prefers-reduced-motion:reduce){.wz *,.wz *::before{transition:none!important;animation:none!important}}
@media (max-width:600px){.wz{padding:24px 18px}.wz-split{padding:0}.wz-vsteps > li{column-gap:12px}}
JavaScript
// Form wizard: shows one step at a time, marks the stepper with data-state and aria-current="step",
// checks the step's fields before moving on and moves focus to the new step's heading.
// Back / Continue can sit in one shared [data-nav] bar or inside every step: one delegated click handler serves both.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li'), last = panels.length - 1;
var nav = wz.querySelector('[data-nav]'), submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
function heading(p) { return document.getElementById(p.getAttribute('aria-labelledby')); }
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
}
function go(i, focus) {
at = i; mark(i);
panels.forEach(function (p, n) { p.hidden = n !== i; });
wz.querySelectorAll('[data-back]').forEach(function (b) { b.hidden = i === 0; });
wz.querySelectorAll('[data-next]').forEach(function (b) { b.hidden = i === last; });
if (submit) submit.hidden = i !== last;
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
if (focus) heading(panels[i]).focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
wz.noValidate = true;
wz.addEventListener('click', function (e) {
var b = e.target.closest('[data-back], [data-next], [data-restart]');
if (!b) return;
if (b.hasAttribute('data-back')) go(at - 1, true);
else if (b.hasAttribute('data-next')) { if (valid()) go(at + 1, true); }
else { wz.reset(); done.hidden = true; if (nav) nav.hidden = false; go(0, true); }
});
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < last) return go(at + 1, true);
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
if (!done) return;
panels[at].hidden = true; if (nav) nav.hidden = true; mark(steps.length);
done.hidden = false; heading(done).focus();
});
go(0);
});
Bootstrap 5 code
Requires: Bootstrap 5.3.8 CSS
HTML
<div><span class="lbl">Steps expand in place</span>
<form class="wz card" data-wizard aria-labelledby="mf-title"><div class="card-body">
<h2 class="wz-title mb-0" id="mf-title">Open your shop on Marketfold</h2>
<p class="text-body-secondary mt-1 mb-0 small">Four steps, about three minutes. Nothing goes live until you open the shop.</p>
<ol class="wz-vsteps" data-stepper aria-label="Progress">
<li>
<span class="wz-dot" aria-hidden="true"><b>1</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h1" tabindex="-1"><span class="wz-kick">Step 1 of 4</span>Store details<span class="visually-hidden" data-sr></span></h3>
<p class="wz-sum"><span data-out="shop">Marketfold Ceramics</span> · <span data-out="currency">USD</span></p>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h1">
<div class="row g-3">
<div class="col-sm-6"><label class="form-label" for="mf-shop">Shop name</label>
<input class="form-control" id="mf-shop" name="shop" required autocomplete="organization" value="Marketfold Ceramics"></div>
<div class="col-sm-6"><label class="form-label" for="mf-currency">Currency</label>
<select class="form-select" id="mf-currency" name="currency"><option value="USD">US dollar (USD)</option><option value="EUR">Euro (EUR)</option><option value="GBP">Pound sterling (GBP)</option><option value="CAD">Canadian dollar (CAD)</option></select></div>
</div>
<div><label class="form-label" for="mf-handle">Shop address</label>
<div class="input-group"><span class="input-group-text">marketfold.com/</span><input class="form-control" id="mf-handle" name="handle" required pattern="[a-z0-9\-]+" aria-describedby="mf-handle-hint" value="marketfold-ceramics"></div>
<div class="form-text" id="mf-handle-hint">Lowercase letters, numbers and hyphens.</div></div>
<div class="wz-actions">
<button type="button" class="btn btn-primary" data-next hidden>Continue</button>
</div>
</section>
</li>
<li>
<span class="wz-dot" aria-hidden="true"><b>2</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h2" tabindex="-1"><span class="wz-kick">Step 2 of 4</span>Shipping<span class="visually-hidden" data-sr></span></h3>
<p class="wz-sum">Ships from <span data-out="from">Portugal</span> · <span data-out="processing">3–5 days</span></p>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h2">
<div><label class="form-label" for="mf-from">Ships from</label>
<select class="form-select" id="mf-from" name="from" required><option>Portugal</option><option>Spain</option><option>United States</option><option>Canada</option><option>United Kingdom</option></select></div>
<fieldset><legend>Processing time</legend>
<div class="wz-seg d-flex flex-wrap gap-2">
<label><input class="form-check-input" type="radio" name="processing" value="1–2 days" required>1–2 days</label>
<label><input class="form-check-input" type="radio" name="processing" value="3–5 days" checked>3–5 days</label>
<label><input class="form-check-input" type="radio" name="processing" value="1–2 weeks">1–2 weeks</label>
</div></fieldset>
<div class="wz-actions">
<button type="button" class="btn btn-primary" data-next hidden>Continue</button>
<button type="button" class="btn btn-outline-secondary" data-back hidden>Back</button>
</div>
</section>
</li>
<li>
<span class="wz-dot" aria-hidden="true"><b>3</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h3" tabindex="-1"><span class="wz-kick">Step 3 of 4</span>Payments<span class="visually-hidden" data-sr></span></h3>
<p class="wz-sum"><span data-out="payout">PayPal</span> · <span data-out="account">[email protected]</span></p>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h3">
<fieldset><legend>Payout method</legend>
<div class="row g-2">
<div class="col-sm-6"><label class="wz-tile h-100"><input class="form-check-input" type="radio" name="payout" value="Bank transfer" required><b>Bank transfer</b><span>Every Friday · no fee</span></label></div>
<div class="col-sm-6"><label class="wz-tile h-100"><input class="form-check-input" type="radio" name="payout" value="PayPal" checked><b>PayPal</b><span>Next working day · 1% fee</span></label></div>
</div></fieldset>
<div><label class="form-label" for="mf-account">IBAN or PayPal email</label>
<input class="form-control" id="mf-account" name="account" required aria-describedby="mf-account-hint" value="[email protected]">
<div class="form-text" id="mf-account-hint">Bank transfer: your IBAN. PayPal: the email on your PayPal account.</div></div>
<div class="wz-actions">
<button type="button" class="btn btn-primary" data-next hidden>Continue</button>
<button type="button" class="btn btn-outline-secondary" data-back hidden>Back</button>
</div>
</section>
</li>
<li>
<span class="wz-dot" aria-hidden="true"><b>4</b><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<div class="wz-head">
<h3 class="wz-h" id="mf-h4" tabindex="-1"><span class="wz-kick">Step 4 of 4</span>Launch<span class="visually-hidden" data-sr></span></h3>
</div>
<section class="wz-body" data-step aria-labelledby="mf-h4">
<p class="wz-url"><span>Your shop will open at</span><b>marketfold.com/<span data-out="handle">marketfold-ceramics</span></b></p>
<p class="text-body-secondary small mb-0">Buyers can find you as soon as you open. You can pause the shop from Settings at any time.</p>
<div class="wz-actions">
<button type="submit" class="btn btn-primary">Open shop</button>
<button type="button" class="btn btn-outline-secondary" data-back hidden>Back</button>
</div>
</section>
</li>
</ol>
<section class="wz-panel wz-done" data-done hidden aria-labelledby="mf-h5">
<span class="wz-dot"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<h3 class="wz-h" id="mf-h5" tabindex="-1"><span data-out="shop">Marketfold Ceramics</span> is open</h3>
<p class="text-body-secondary small mb-0">Add your first listing and it appears at marketfold.com/<span data-out="handle">marketfold-ceramics</span>.</p>
<button type="button" class="btn btn-outline-secondary" data-restart>Set up another shop</button>
</section>
</div></form></div>
<div><span class="lbl">Side rail with descriptions</span>
<form class="wz card wz-split" data-wizard aria-labelledby="gr-title">
<div class="wz-rail">
<h2 class="wz-title mb-0" id="gr-title">Grant application</h2>
<p class="text-body-secondary mt-1 mb-0 small">Riverside Arts Council · Spring 2027 round</p>
<ol class="wz-rsteps" data-stepper aria-label="Progress">
<li><span class="wz-dot"><b>1</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Applicant</span><span class="wz-desc">Who is applying and how to reach you</span></span><span class="visually-hidden" data-sr></span></li>
<li><span class="wz-dot"><b>2</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Project</span><span class="wz-desc">What you will make and when</span></span><span class="visually-hidden" data-sr></span></li>
<li><span class="wz-dot"><b>3</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Budget</span><span class="wz-desc">The amount you ask for</span></span><span class="visually-hidden" data-sr></span></li>
<li><span class="wz-dot"><b>4</b><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span><span class="wz-rtext"><span class="wz-name">Attachments</span><span class="wz-desc">Samples and sign-off</span></span><span class="visually-hidden" data-sr></span></li>
</ol>
</div>
<div class="wz-main">
<section class="wz-panel" data-step aria-labelledby="gr-h1">
<h3 class="wz-h" id="gr-h1" tabindex="-1"><span class="wz-kick">Step 1 of 4</span>About the applicant</h3>
<p class="wz-lead">Individuals and groups based in Riverside County can apply.</p>
<div class="row g-3">
<div class="col-sm-6"><label class="form-label" for="gr-name">Full name</label>
<input class="form-control" id="gr-name" name="name" required autocomplete="name" value="Amara Okafor"></div>
<div class="col-sm-6"><label class="form-label" for="gr-org">Organisation</label>
<input class="form-control" id="gr-org" name="organisation" autocomplete="organization" aria-describedby="gr-org-hint" value="Tidewater Print Collective">
<div class="form-text" id="gr-org-hint">Leave empty if you apply as an individual.</div></div>
</div>
<div><label class="form-label" for="gr-email">Email</label>
<input class="form-control" id="gr-email" name="email" type="email" required autocomplete="email" value="[email protected]"></div>
</section>
<section class="wz-panel" data-step aria-labelledby="gr-h2">
<h3 class="wz-h" id="gr-h2" tabindex="-1"><span class="wz-kick">Step 2 of 4</span>Your project</h3>
<div><label class="form-label" for="gr-project">Project title</label>
<input class="form-control" id="gr-project" name="project" required value="Riverside Linocut Trail"></div>
<div><label class="form-label" for="gr-summary">Summary</label>
<textarea class="form-control" id="gr-summary" name="summary" rows="4" required minlength="80" aria-describedby="gr-summary-hint">Twelve large linocut panels printed with residents of the Eastbank estate and mounted along the river path between Mill Bridge and the ferry landing.</textarea>
<div class="form-text" id="gr-summary-hint">At least 80 characters: what you will make, where, and with whom.</div></div>
<div style="max-width:240px"><label class="form-label" for="gr-start">Start month</label>
<input class="form-control" id="gr-start" name="start" type="month" required min="2027-01" value="2027-03"></div>
</section>
<section class="wz-panel" data-step aria-labelledby="gr-h3">
<h3 class="wz-h" id="gr-h3" tabindex="-1"><span class="wz-kick">Step 3 of 4</span>Budget</h3>
<div class="row g-3">
<div class="col-sm-6"><label class="form-label" for="gr-amount">Amount requested</label>
<div class="input-group"><span class="input-group-text">$</span><input class="form-control" id="gr-amount" name="amount" type="number" required min="500" max="15000" step="50" aria-describedby="gr-amount-hint" value="9800"></div>
<div class="form-text" id="gr-amount-hint">Between $500 and $15,000.</div></div>
<div class="col-sm-6"><label class="form-label" for="gr-other">Other funding</label>
<select class="form-select" id="gr-other" name="other"><option>None</option><option>Applied, awaiting a decision</option><option>Confirmed</option></select></div>
</div>
</section>
<section class="wz-panel" data-step aria-labelledby="gr-h4">
<h3 class="wz-h" id="gr-h4" tabindex="-1"><span class="wz-kick">Step 4 of 4</span>Attachments</h3>
<div><label class="form-label" for="gr-files">Work samples (optional)</label>
<input class="form-control" id="gr-files" name="samples" type="file" multiple accept=".pdf,.jpg,.jpeg,application/pdf,image/jpeg" aria-describedby="gr-files-hint">
<div class="form-text" id="gr-files-hint">PDF or JPG. Photos of earlier work help the panel.</div></div>
<div class="form-check"><input class="form-check-input" type="checkbox" name="confirm" id="gr-confirm" required><label class="form-check-label" for="gr-confirm">I confirm the information is accurate</label></div>
</section>
<section class="wz-panel wz-done" data-done hidden aria-labelledby="gr-h5">
<span class="wz-dot"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>
<h3 class="wz-h" id="gr-h5" tabindex="-1">Application received</h3>
<p class="wz-ref">Reference <b>RAC-2026-0417</b></p>
<p class="text-body-secondary small mb-0">We sent a copy to <span data-out="email">[email protected]</span>. The council announces spring awards on 15 January 2027.</p>
<button type="button" class="btn btn-outline-secondary" data-restart>Start a new application</button>
</section>
<div class="wz-nav" data-nav>
<button type="button" class="btn btn-outline-secondary" data-back hidden>Back</button>
<button type="button" class="btn btn-primary" data-next hidden>Continue</button>
<button type="submit" class="btn btn-primary">Submit application</button>
</div>
</div>
</form></div>
CSS
/* Vertical wizard on Bootstrap 5: .card, .form-control, .form-select, .input-group, .form-check and .btn,
restyled through Bootstrap's CSS variables. The vertical stepper and the side rail are the custom parts.
The script hides every step but the current one, sets data-state and aria-current="step" on the stepper
and moves focus to the new step's heading. Without JavaScript every step shows and the form submits normally. */
.wz{--wz-accent:#047857;--wz-accent-dark:#065f46;--wz-soft:#d1fae5;--wz-tint:#ecfdf5;--wz-muted:#56615a;--wz-line:#808c84;--wz-rail:#f6f8f4;
--bs-border-color:#78847c;--bs-border-radius:10px;--bs-body-color:#14201a;--bs-secondary-color:#56615a;
border:0;border-radius:18px;box-shadow:0 1px 2px rgba(20,32,26,.06),0 12px 32px -14px rgba(6,78,59,.22)}
.wz .card-body{padding:30px 32px}
.wz .form-control,.wz .form-select{border-width:1.5px;min-height:44px;padding:10px 12px}
.wz .form-select{padding-right:36px}
.wz .form-control:focus,.wz .form-select:focus{border-color:var(--wz-accent);box-shadow:0 0 0 4px var(--wz-soft)}
.wz textarea.form-control{min-height:108px}
.wz .form-control[type="file"]{padding:6px}
.wz .form-control::file-selector-button{margin:0 12px 0 0;padding:7px 14px;border:0;border-radius:7px;background:var(--wz-soft);color:var(--wz-accent-dark);font-size:.88rem;font-weight:600}
.wz .form-control:hover:not(:disabled):not([readonly])::file-selector-button{background:#bbf2d6}
.wz .input-group-text{border-width:1.5px;background:var(--wz-rail);color:#4a554e;font-size:.9rem}
.wz .input-group:has(:focus) .input-group-text{border-color:var(--wz-accent)}
.wz .form-label,.wz legend{font-size:.88rem;font-weight:600;margin-bottom:6px}
.wz .form-text{font-size:.8rem;color:var(--wz-muted);margin-top:6px}
.wz .form-check-input{border-width:1.5px;border-color:var(--wz-line)}
.wz .form-check-input:checked{background-color:var(--wz-accent);border-color:var(--wz-accent)}
.wz .form-check-input:focus{border-color:var(--wz-accent);box-shadow:0 0 0 3px var(--wz-soft)}
.wz .form-check-input:focus-visible{outline:2px solid var(--wz-accent);outline-offset:2px}
.wz .btn{--bs-btn-padding-x:22px;--bs-btn-font-weight:600;--bs-btn-border-width:1.5px;min-height:44px}
.wz .btn-primary{--bs-btn-bg:var(--wz-accent);--bs-btn-border-color:var(--wz-accent);--bs-btn-hover-bg:var(--wz-accent-dark);--bs-btn-hover-border-color:var(--wz-accent-dark);--bs-btn-active-bg:var(--wz-accent-dark);--bs-btn-active-border-color:var(--wz-accent-dark)}
.wz .btn-outline-secondary{--bs-btn-color:var(--wz-accent-dark);--bs-btn-border-color:var(--wz-line);--bs-btn-hover-bg:var(--wz-tint);--bs-btn-hover-color:var(--wz-accent-dark);--bs-btn-hover-border-color:var(--wz-line);--bs-btn-active-bg:var(--wz-soft);--bs-btn-active-color:var(--wz-accent-dark);--bs-btn-bg:#fff}
.wz .btn:focus-visible{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--wz-accent);outline:0}
.wz-title{font-size:1.3rem;font-weight:700;letter-spacing:-.015em}
.wz-dot{flex:none;width:36px;height:36px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--wz-line);background:#fff;font-weight:700;font-size:.9rem;color:#4a554e;transition:background .25s,border-color .25s,box-shadow .25s}
.wz-dot svg{display:none;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.wz [data-state="current"] > .wz-dot{border-color:var(--wz-accent);color:var(--wz-accent-dark);box-shadow:0 0 0 5px var(--wz-soft)}
.wz [data-state="done"] > .wz-dot{border-color:var(--wz-accent);background:var(--wz-accent);color:#fff}
.wz [data-state="done"] > .wz-dot b{display:none}
.wz [data-state="done"] > .wz-dot svg{display:block}
/* expand in place: each <li> is dot + header + the step's section; a line joins the dots and fills when a step is done */
.wz-vsteps{list-style:none;margin:26px 0 0;padding:0}
.wz-vsteps > li{position:relative;display:grid;grid-template-columns:36px minmax(0,1fr);column-gap:16px;padding-bottom:22px}
.wz-vsteps > li:last-child{padding-bottom:0}
.wz-vsteps > li:not(:last-child)::before{content:"";position:absolute;left:17px;top:46px;bottom:8px;width:2px;border-radius:2px;background:var(--wz-line);transition:background .25s}
.wz-vsteps > li[data-state="done"]::before{background:var(--wz-accent)}
.wz-vsteps .wz-dot{margin-top:4px}
.wz-head{min-width:0}
.wz-head .wz-h{font-size:1.02rem}
.wz-vsteps [data-state="next"] .wz-h,.wz-vsteps [data-state="next"] .wz-kick{color:var(--wz-muted)}
.wz-vsteps [data-state="next"] .wz-h{font-weight:600}
.wz-sum{display:none;margin:2px 0 0;font-size:.86rem;color:var(--wz-muted);overflow-wrap:anywhere}
.wz-vsteps [data-state="done"] .wz-sum{display:block}
.wz-body{grid-column:2;display:grid;gap:16px;padding-top:16px;animation:wz-in .3s ease}
.wz-actions{display:flex;flex-wrap:wrap;gap:10px;padding-top:4px}
.wz-url{display:grid;gap:4px;margin:0;padding:16px 18px;border-radius:12px;background:var(--wz-tint);border:1px solid #a7e3c9}
.wz-url > span{font-size:.8rem;color:var(--wz-muted)}
.wz-url b{font-size:1.02rem;color:var(--wz-accent-dark);overflow-wrap:anywhere}
/* side rail: steps with descriptions on the left, the current step on the right */
.wz-split{display:grid;grid-template-columns:250px minmax(0,1fr);overflow:hidden}
.wz-rail{background:var(--wz-rail);border-right:1px solid #e1e7df;padding:30px 24px}
.wz-main{padding:30px 32px;display:flex;flex-direction:column;min-width:0}
.wz-rsteps{list-style:none;margin:28px 0 0;padding:0}
.wz-rsteps li{position:relative;display:flex;gap:14px;padding-bottom:26px;color:var(--wz-muted)}
.wz-rsteps li:last-child{padding-bottom:0}
.wz-rsteps li:not(:last-child)::before{content:"";position:absolute;left:17px;top:42px;bottom:6px;width:2px;border-radius:2px;background:var(--wz-line);transition:background .25s}
.wz-rsteps li[data-state="done"]::before{background:var(--wz-accent)}
.wz-rtext{display:grid;gap:1px;padding-top:6px;min-width:0}
.wz-name{font-size:.92rem;font-weight:600}
.wz-desc{font-size:.8rem;line-height:1.4}
.wz-rsteps [data-state="current"],.wz-rsteps [data-state="done"]{color:var(--bs-body-color)}
.wz-rsteps [data-state="done"] .wz-desc,.wz-rsteps [data-state="current"] .wz-desc{color:var(--wz-muted)}
@media (max-width:640px){
.wz-split{grid-template-columns:1fr}
.wz-rail{border-right:0;border-bottom:1px solid #e1e7df;padding:24px 18px 18px}
.wz-main{padding:24px 18px}
.wz-rsteps{display:flex;align-items:center;gap:8px;margin-top:18px}
.wz-rsteps li{padding:0;align-items:center;gap:10px}
.wz-rsteps li::before,.wz-desc{display:none}
.wz-rsteps .wz-dot{width:30px;height:30px;font-size:.8rem}
.wz-rtext{padding:0}
.wz-rsteps [data-state]:not([data-state="current"]) .wz-rtext{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}}
/* step panels */
.wz-panel{display:grid;gap:18px;animation:wz-in .3s ease}
@keyframes wz-in{from{opacity:0;transform:translateY(6px)}}
.wz-h{margin:0;font-size:1.15rem;font-weight:700;letter-spacing:-.01em;line-height:1.5}
.wz-h:focus{outline:none}
.wz-kick{display:block;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--wz-accent-dark);margin-bottom:2px}
.wz-lead{margin:-10px 0 0;color:var(--wz-muted);font-size:.92rem}
/* choice pills and tiles: a .form-check-input inside each label, the outline follows :has(:checked) */
.wz-seg label{display:flex;align-items:center;gap:8px;padding:9px 14px;border:1.5px solid var(--bs-border-color);border-radius:999px;font-size:.9rem;cursor:pointer;transition:border-color .2s,background .2s}
.wz-seg .form-check-input{width:16px;height:16px;margin:0}
.wz-seg label:has(:checked){border-color:var(--wz-accent);background:var(--wz-tint);color:var(--wz-accent-dark);font-weight:600}
.wz-tile{position:relative;display:grid;gap:2px;padding:14px 14px 14px 44px;border:1.5px solid var(--bs-border-color);border-radius:12px;cursor:pointer;transition:border-color .2s,background .2s}
.wz-tile .form-check-input{position:absolute;left:14px;top:16px;width:18px;height:18px;margin:0}
.wz-tile b{font-size:.95rem}
.wz-tile span{font-size:.82rem;color:var(--wz-muted)}
.wz-tile:has(:checked){border-color:var(--wz-accent);background:var(--wz-tint)}
.wz .form-check{display:flex;align-items:flex-start;gap:10px;padding-left:0;font-size:.92rem}
.wz .form-check .form-check-input{flex:none;width:18px;height:18px;margin:2px 0 0;float:none}
/* buttons */
.wz-nav{display:flex;gap:8px;margin-top:auto;padding-top:20px;border-top:1px solid #e6ebe4}
.wz-main > .wz-panel{margin-bottom:28px}
.wz-nav .btn-primary{margin-left:auto}
.wz-done{text-align:center;justify-items:center;padding:12px 0 4px}
.wz-vsteps + .wz-done{margin-top:26px;padding-top:26px;border-top:1px solid #e6ebe4}
.wz-done .wz-dot{width:56px;height:56px;border:0;background:var(--wz-soft);color:var(--wz-accent-dark)}
.wz-done .wz-dot svg{display:block;width:26px;height:26px}
.wz-ref{margin:0;padding:8px 16px;border-radius:999px;background:var(--wz-tint);border:1px dashed var(--wz-accent);font-size:.9rem;color:var(--wz-accent-dark)}
@media (prefers-reduced-motion:reduce){.wz *,.wz *::before{transition:none!important;animation:none!important}}
@media (max-width:600px){.wz .card-body{padding:24px 18px}.wz-vsteps > li{column-gap:12px}}
JavaScript
// Form wizard: shows one step at a time, marks the stepper with data-state and aria-current="step",
// checks the step's fields before moving on and moves focus to the new step's heading.
// Back / Continue can sit in one shared [data-nav] bar or inside every step: one delegated click handler serves both.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li'), last = panels.length - 1;
var nav = wz.querySelector('[data-nav]'), submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
function heading(p) { return document.getElementById(p.getAttribute('aria-labelledby')); }
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
}
function go(i, focus) {
at = i; mark(i);
panels.forEach(function (p, n) { p.hidden = n !== i; });
wz.querySelectorAll('[data-back]').forEach(function (b) { b.hidden = i === 0; });
wz.querySelectorAll('[data-next]').forEach(function (b) { b.hidden = i === last; });
if (submit) submit.hidden = i !== last;
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
if (focus) heading(panels[i]).focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
wz.noValidate = true;
wz.addEventListener('click', function (e) {
var b = e.target.closest('[data-back], [data-next], [data-restart]');
if (!b) return;
if (b.hasAttribute('data-back')) go(at - 1, true);
else if (b.hasAttribute('data-next')) { if (valid()) go(at + 1, true); }
else { wz.reset(); done.hidden = true; if (nav) nav.hidden = false; go(0, true); }
});
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < last) return go(at + 1, true);
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
if (!done) return;
panels[at].hidden = true; if (nav) nav.hidden = true; mark(steps.length);
done.hidden = false; heading(done).focus();
});
go(0);
});
// Form wizard: shows one step at a time, marks the stepper with data-state and aria-current="step",
// checks the step's fields before moving on and moves focus to the new step's heading.
// Back / Continue can sit in one shared [data-nav] bar or inside every step: one delegated click handler serves both.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li'), last = panels.length - 1;
var nav = wz.querySelector('[data-nav]'), submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
function heading(p) { return document.getElementById(p.getAttribute('aria-labelledby')); }
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
}
function go(i, focus) {
at = i; mark(i);
panels.forEach(function (p, n) { p.hidden = n !== i; });
wz.querySelectorAll('[data-back]').forEach(function (b) { b.hidden = i === 0; });
wz.querySelectorAll('[data-next]').forEach(function (b) { b.hidden = i === last; });
if (submit) submit.hidden = i !== last;
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
if (focus) heading(panels[i]).focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
wz.noValidate = true;
wz.addEventListener('click', function (e) {
var b = e.target.closest('[data-back], [data-next], [data-restart]');
if (!b) return;
if (b.hasAttribute('data-back')) go(at - 1, true);
else if (b.hasAttribute('data-next')) { if (valid()) go(at + 1, true); }
else { wz.reset(); done.hidden = true; if (nav) nav.hidden = false; go(0, true); }
});
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < last) return go(at + 1, true);
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
if (!done) return;
panels[at].hidden = true; if (nav) nav.hidden = true; mark(steps.length);
done.hidden = false; heading(done).focus();
});
go(0);
});
About this set
Two vertical multi-step forms that keep every step in view. Steps expand in place is a Material-style stepper where the ordered list is the layout: each list item holds a numbered dot, the step title and the step’s fields right underneath, with Continue and Back buttons inside the open step. Finished steps collapse to a one-line summary of what was entered, such as the shop name and currency, and the connector line between the dots turns green as you go. Use it for short setup flows on a single column, like opening a shop. Side rail with descriptions puts the steps in a left column with a title and a one-line description each, and shows only the current step on the right with a shared Back and Continue bar. It suits longer applications where people want to see what is coming, and under 640px the rail turns into a compact row of dots with the current title. One small vanilla script drives both: it shows one step at a time, checks the step’s fields with the browser’s own validation before moving on, marks the stepper with aria-current and moves focus to the new step’s heading. Without JavaScript every step shows and the form submits normally.
What’s included
Expand-in-place stepper with per-step Continue and Back buttons and a summary line for finished steps
Side rail with step descriptions that collapses to a row of dots on phones
Native checkValidity and reportValidity per step; Enter in a field acts as Continue
Review slots and done panels filled from the form's own values
Plain CSS, Bootstrap 5.3.8 and Tailwind CSS 4 versions share one identical script
Accessibility
The stepper is an ordered list labelled Progress; the current step carries aria-current="step" and finished steps get a visually hidden ", completed". Each step is a section named by its heading, which starts with a Step 2 of 4 kicker and receives focus on Back and Continue, so screen readers announce where you are. Every control has a visible label and a visible focus style, field borders and text meet WCAG AA contrast, and transitions and the step fade are switched off under prefers-reduced-motion.
Customise it
Change the --wz-accent, --wz-accent-dark, --wz-soft and --wz-tint custom properties on .wz in the plain CSS and Bootstrap versions (Bootstrap also maps --bs-border-color there), or the --color-wz-* values in the Tailwind @theme block.