Open demo

CSS code

HTML
<div><span class="lbl">Numbered stepper checkout</span>
<form class="st" action="#" method="post" data-checkout data-steps data-subtotal="281.00" aria-label="Checkout">
  <div class="st-layout">
    <div class="st-card">
      <p class="st-brand">Marlow <em>&</em> Fine</p>
      <nav aria-label="Checkout progress"><ol class="st-steps">
        <li class="st-step" data-step-mark data-state="current" aria-current="step"><span class="st-dot" aria-hidden="true"><span>1</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Details<span class="sr-only" data-step-sr></span></li>
        <li class="st-step" data-step-mark data-state="next"><span class="st-dot" aria-hidden="true"><span>2</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Delivery<span class="sr-only" data-step-sr></span></li>
        <li class="st-step" data-step-mark data-state="next"><span class="st-dot" aria-hidden="true"><span>3</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Payment<span class="sr-only" data-step-sr></span></li>
        <li class="st-step" data-step-mark data-state="next"><span class="st-dot" aria-hidden="true"><span>4</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Review<span class="sr-only" data-step-sr></span></li>
      </ol></nav>
        <section class="st-panel" data-step-panel aria-labelledby="st-h1">
          <h2 class="st-h" id="st-h1" tabindex="-1">Your details</h2>
          <p class="st-sub">Where should we send your order?</p>
          <div class="st-grid">
            <div class="st-s6 st-m6"><label class="st-label" for="st-email">Email</label><input class="st-input" id="st-email" name="email" type="email" autocomplete="email" required aria-describedby="st-email-hint st-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected]."><p class="st-hint" id="st-email-hint">For your receipt and tracking link.</p><p class="st-err" id="st-email-err"></p></div>
            <div class="st-s6 st-m6"><label class="st-label" for="st-name">Full name</label><input class="st-input" id="st-name" name="name" type="text" autocomplete="shipping name" required aria-describedby="st-name-err" data-missing="Enter your full name."><p class="st-err" id="st-name-err"></p></div>
            <div class="st-s6 st-m6"><label class="st-label" for="st-address">Street address</label><input class="st-input" id="st-address" name="address" type="text" autocomplete="shipping address-line1" required aria-describedby="st-address-err" data-missing="Enter your street address."><p class="st-err" id="st-address-err"></p></div>
            <div class="st-s2 st-m6"><label class="st-label" for="st-city">City</label><input class="st-input" id="st-city" name="city" type="text" autocomplete="shipping address-level2" required aria-describedby="st-city-err" data-missing="Enter your city."><p class="st-err" id="st-city-err"></p></div>
            <div class="st-s2 st-m3"><label class="st-label" for="st-state">State</label><input class="st-input" id="st-state" name="state" type="text" autocomplete="shipping address-level1" required aria-describedby="st-state-err" data-missing="Enter your state."><p class="st-err" id="st-state-err"></p></div>
            <div class="st-s2 st-m3"><label class="st-label" for="st-zip">ZIP code</label><input class="st-input" id="st-zip" name="zip" type="text" autocomplete="shipping postal-code" inputmode="numeric" pattern="[0-9]{5}(-[0-9]{4})?" required aria-describedby="st-zip-err" data-missing="Enter your ZIP code." data-bad="Enter a 5-digit ZIP code, like 02118."><p class="st-err" id="st-zip-err"></p></div>
          </div>
          <div class="st-nav"><button class="st-btn" type="button" data-next hidden>Continue to delivery</button></div>
        </section>
        <section class="st-panel" data-step-panel aria-labelledby="st-h2">
          <h2 class="st-h" id="st-h2" tabindex="-1">Delivery</h2>
          <p class="st-sub">Every parcel is insured and needs a signature.</p>
          <fieldset class="st-options"><legend class="sr-only">Delivery method</legend>
            <label class="st-radio"><input type="radio" name="delivery" value="std" data-ship-option data-label="Insured standard" data-price="8.00" checked><span><strong>Insured standard</strong><span>3 to 5 business days, signature on delivery</span></span><b>$8.00</b></label>
            <label class="st-radio"><input type="radio" name="delivery" value="exp" data-ship-option data-label="Insured express" data-price="18.00"><span><strong>Insured express</strong><span>Next business day, signature on delivery</span></span><b>$18.00</b></label>
            <label class="st-radio"><input type="radio" name="delivery" value="collect" data-ship-option data-label="Collect from the studio" data-price="0.00"><span><strong>Collect from the studio</strong><span>Ready tomorrow · 4 Wharf Lane</span></span><b>Free</b></label>
          </fieldset>
          <div class="st-nav"><button class="st-btn st-btn--ghost" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="st-btn" type="button" data-next hidden>Continue to payment</button></div>
        </section>
        <section class="st-panel" data-step-panel aria-labelledby="st-h3">
          <h2 class="st-h" id="st-h3" tabindex="-1">Payment</h2>
          <p class="st-sub">Card details for this demo checkout.</p>
          <div class="st-demo"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8-8 9-4.6-1-8-4.5-8-9V6z"/><path d="M12 8v4M12 16h.01"/></svg><p><strong>Demo payment form</strong>Not connected to a payment provider. Nothing you type is sent or stored and no card is charged.</p></div>
          <div class="st-grid">
            <div class="st-s6 st-m6"><label class="st-label" for="st-card">Card number</label><input class="st-input" id="st-card" type="text" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]{13,23}" placeholder="1234 1234 1234 1234" required aria-describedby="st-card-err" data-missing="Enter a card number." data-bad="Enter the 13 to 19 digits on the front of the card."><p class="st-err" id="st-card-err"></p></div>
            <div class="st-s3 st-m3"><label class="st-label" for="st-exp">Expiry date</label><input class="st-input" id="st-exp" type="text" autocomplete="cc-exp" inputmode="numeric" pattern="(0[1-9]|1[0-2]) ?/ ?[0-9]{2}" placeholder="MM / YY" required aria-describedby="st-exp-err" data-missing="Enter the expiry date." data-bad="Use the format MM / YY."><p class="st-err" id="st-exp-err"></p></div>
            <div class="st-s3 st-m3"><label class="st-label" for="st-cvc">Security code</label><input class="st-input" id="st-cvc" type="text" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]{3,4}" required aria-describedby="st-cvc-hint st-cvc-err" data-missing="Enter the security code." data-bad="Enter the 3 or 4 digit code."><p class="st-hint" id="st-cvc-hint">3 or 4 digits on the back.</p><p class="st-err" id="st-cvc-err"></p></div>
            <div class="st-s6 st-m6"><label class="st-label" for="st-ccname">Name on card</label><input class="st-input" id="st-ccname" type="text" autocomplete="cc-name" required aria-describedby="st-ccname-err" data-missing="Enter the name on the card."><p class="st-err" id="st-ccname-err"></p></div>
          </div>
          <div class="st-nav"><button class="st-btn st-btn--ghost" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="st-btn" type="button" data-next hidden>Continue to review</button></div>
        </section>
        <section class="st-panel" data-step-panel aria-labelledby="st-h4">
          <h2 class="st-h" id="st-h4" tabindex="-1">Review your order</h2>
          <p class="st-sub">Check everything before you place the order.</p>
          <dl class="st-review">
            <div><dt>Email</dt><dd><span data-review="st-email">Not given</span></dd></div>
            <div><dt>Ship to</dt><dd><span data-review="st-name">Not given</span>, <span data-review="st-address">Not given</span>, <span data-review="st-city">Not given</span> <span data-review="st-zip">Not given</span></dd></div>
            <div><dt>Delivery</dt><dd><span data-ship-label>Insured standard</span></dd></div>
            <div><dt>Payment</dt><dd><span data-review-card="st-card">Not given</span> (demo)</dd></div>
          </dl>
          <p class="st-status" role="status" data-status></p>
          <div class="st-nav"><button class="st-btn st-btn--ghost" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="st-btn" type="submit"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>Place demo order</button></div>
        </section>
    </div>
    <aside class="st-card st-sum" aria-labelledby="st-sum-title">
      <h2 id="st-sum-title">Order summary</h2>
      <ul class="st-items">
        <li class="st-item"><img src="https://images.unsplash.com/photo-1603561591411-07134e71a2a9?w=104&h=104&fit=crop&auto=format&q=70" alt="" width="52" height="52"><div><p>Pink tourmaline ring</p><p class="st-meta">Rose gold · Size 6 · Qty 1</p></div><p class="st-price">$185.00</p></li>
        <li class="st-item"><img src="https://images.unsplash.com/photo-1535632066927-ab7c9ab60908?w=104&h=104&fit=crop&auto=format&q=70" alt="" width="52" height="52"><div><p>Blue topaz drop earrings</p><p class="st-meta">Sterling silver · Qty 1</p></div><p class="st-price">$96.00</p></li>
      </ul>
      <dl class="st-rows">
        <div class="st-row"><dt>Subtotal</dt><dd data-subtotal>$281.00</dd></div>
        <div class="st-row"><dt>Delivery</dt><dd data-shipping>$8.00</dd></div>
      </dl>
      <dl class="st-total"><dt>Total</dt><dd data-total>$289.00</dd></dl>
    </aside>
  </div>
</form>
</div>
<div class="st st-demo-row">
<div><span class="lbl">Compact progress bar</span>
<nav class="st-card st-compact" aria-label="Checkout progress, compact">
  <p><strong>Step 2 of 4: Delivery</strong><span>Next: Payment</span></p>
  <ol class="st-bars">
    <li data-state="done"><span class="sr-only">Details, completed</span></li>
    <li data-state="current" aria-current="step"><span class="sr-only">Delivery</span></li>
    <li data-state="next"><span class="sr-only">Payment</span></li>
    <li data-state="next"><span class="sr-only">Review</span></li>
  </ol>
</nav>
</div>
<div><span class="lbl">Vertical steps</span>
<nav class="st-card" aria-label="Checkout progress, vertical">
  <ol class="st-vert">
    <li data-state="done"><span class="st-dot" aria-hidden="true"><span>1</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Details<span class="sr-only">, completed</span></strong><small>[email protected]</small></div></li>
    <li data-state="done"><span class="st-dot" aria-hidden="true"><span>2</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Delivery<span class="sr-only">, completed</span></strong><small>Insured express, next business day</small></div></li>
    <li data-state="current" aria-current="step"><span class="st-dot" aria-hidden="true"><span>3</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Payment</strong><small>Card details</small></div></li>
    <li data-state="next"><span class="st-dot" aria-hidden="true"><span>4</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Review</strong><small>Check and place the order</small></div></li>
  </ol>
</nav>
</div>
</div>
CSS
/* Multi-step checkout. A single <form> split into step panels; the progress list marks the current step with
   aria-current="step" and finished steps with visually hidden "completed" text. Card fields are a front-end mock
   with no name attribute, and the demo submit is stopped by the script. */
.st{--st-accent:#7a2e4f;--st-accent-dark:#5f2240;--st-ink:#22161c;--st-muted:#625560;--st-line:#ebe3e7;--st-field:#8a7d85;--st-soft:#faf5f7;
  --st-ring:rgba(122,46,79,.25);--st-error:#b42318;--st-ok:#166534;color:var(--st-ink);font-size:.93rem;line-height:1.45}
.st [hidden]{display:none!important}
.st .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}
.st :where(button,input){font:inherit;color:inherit}
.st :is(a,button):focus-visible,.st-h:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.st-h:focus:not(:focus-visible){outline:none}
.st-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:28px;align-items:start}
.st-card{background:#fff;border-radius:18px;padding:28px;box-shadow:0 1px 2px rgba(34,22,28,.05),0 16px 40px -24px rgba(34,22,28,.32)}
.st-brand{font-family:Georgia,"Times New Roman",serif;font-size:1.3rem;margin-bottom:20px}
.st-brand em{color:var(--st-accent)}
/* numbered step indicator */
.st-steps{list-style:none;display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:26px;counter-reset:none}
.st-step{position:relative;display:grid;justify-items:center;gap:6px;text-align:center;font-size:.8rem;font-weight:600;color:var(--st-muted)}
.st-step::before{content:"";position:absolute;top:15px;right:50%;left:-50%;height:2px;background:var(--st-line)}
.st-step:first-child::before{display:none}
.st-step[data-state="done"]::before,.st-step[data-state="current"]::before{background:var(--st-accent)}
.st-dot{position:relative;z-index:1;width:32px;height:32px;border-radius:50%;border:2px solid var(--st-field);background:#fff;display:grid;place-items:center;font-size:.82rem}
.st-dot svg{display:none}
.st-step[data-state="current"]{color:var(--st-ink)}
.st-step[data-state="current"] .st-dot{border-color:var(--st-accent);color:var(--st-accent);box-shadow:0 0 0 4px var(--st-ring)}
.st-step[data-state="done"] .st-dot{border-color:var(--st-accent);background:var(--st-accent);color:#fff}
.st-step[data-state="done"] .st-dot span{display:none}
.st-step[data-state="done"] .st-dot svg{display:block}
.st-panel:not([hidden])+.st-panel{margin-top:28px;padding-top:24px;border-top:1px solid var(--st-line)}
.st-h{font-size:1.2rem;font-weight:700;margin-bottom:4px}
.st-sub{color:var(--st-muted);margin-bottom:18px}
.st-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px}
.st-s6{grid-column:span 6}.st-s3{grid-column:span 3}.st-s2{grid-column:span 2}
.st-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.st-input{display:block;width:100%;padding:11px 13px;border:1px solid var(--st-field);border-radius:10px;background:#fff;transition:border-color .15s,box-shadow .15s}
.st-input::placeholder{color:#7f727a}
.st-input:focus{outline:none;border-color:var(--st-accent);box-shadow:0 0 0 3px var(--st-ring)}
.st-input[aria-invalid="true"]{border-color:var(--st-error)}
.st-input[aria-invalid="true"]:focus{box-shadow:0 0 0 3px rgba(180,35,24,.2)}
.st-hint{font-size:.78rem;color:var(--st-muted);margin-top:5px}
.st-err{display:flex;gap:6px;color:var(--st-error);font-size:.8rem;font-weight:600;margin-top:6px}
.st-err::before{content:"!";flex:none;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--st-error);color:#fff;font-size:.7rem;font-weight:800;display:grid;place-items:center}
.st-err:empty{display:none}
.st-options{border:0;padding:0;margin:0;display:grid;gap:10px}
.st-radio{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:start;padding:14px 16px;border:1px solid var(--st-field);border-radius:12px;cursor:pointer;transition:border-color .15s,background-color .15s}
.st-radio:hover{border-color:var(--st-ink)}
.st-radio:has(:checked){border-color:var(--st-accent);background:var(--st-soft);box-shadow:inset 0 0 0 1px var(--st-accent)}
.st-radio input{width:18px;height:18px;margin:1px 0 0;accent-color:var(--st-accent);cursor:pointer}
.st-radio input:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.st-radio strong{display:block;font-weight:600}
.st-radio span span{font-size:.82rem;color:var(--st-muted)}
.st-radio b{font-variant-numeric:tabular-nums}
.st-demo{display:flex;gap:12px;padding:14px 16px;margin-bottom:16px;border-radius:12px;background:#fff7e6;border:1px solid #e7c98f;color:#5c3b06;font-size:.85rem}
.st-demo svg{flex:none;margin-top:1px}
.st-demo strong{display:block}
.st-review{display:grid;gap:0;border:1px solid var(--st-line);border-radius:12px}
.st-review div{display:grid;grid-template-columns:110px minmax(0,1fr);gap:12px;padding:12px 16px}
.st-review div+div{border-top:1px solid var(--st-line)}
.st-review dt{color:var(--st-muted);font-size:.84rem}
.st-review dd{font-weight:600;overflow-wrap:anywhere}
.st-nav{display:flex;justify-content:space-between;gap:12px;margin-top:22px}
.st-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;border:1px solid var(--st-accent);border-radius:999px;background:var(--st-accent);color:#fff;
  font-weight:700;cursor:pointer;transition:background-color .15s;margin-left:auto}
.st-btn:hover{background:var(--st-accent-dark)}
.st-btn--ghost{background:#fff;color:var(--st-accent);margin-left:0}
.st-btn--ghost:hover{background:var(--st-soft)}
.st-status:not(:empty){margin-top:14px;padding:12px 14px;border-radius:10px;background:#f0fdf4;color:var(--st-ok);font-weight:600;font-size:.85rem}
/* summary */
.st-sum{position:sticky;top:20px;padding:22px}
.st-sum h2{font-size:1.05rem;font-weight:700;margin-bottom:6px}
.st-items{list-style:none;border-bottom:1px solid var(--st-line);padding-bottom:6px}
.st-item{display:grid;grid-template-columns:52px minmax(0,1fr) auto;gap:12px;align-items:center;padding:8px 0}
.st-item img{width:52px;height:52px;border-radius:10px;object-fit:cover;background:var(--st-soft)}
.st-item p:first-child{font-weight:600;font-size:.88rem;line-height:1.3}
.st-meta{font-size:.78rem;color:var(--st-muted)}
.st-price{font-weight:600;font-variant-numeric:tabular-nums}
.st-rows{display:grid;gap:8px;padding:14px 0;border-bottom:1px solid var(--st-line)}
.st-row{display:flex;justify-content:space-between;gap:12px}
.st-row dt{color:var(--st-muted)}
.st-row dd{font-weight:600;font-variant-numeric:tabular-nums}
.st-total{display:flex;justify-content:space-between;align-items:baseline;padding-top:14px;font-weight:700}
.st-total dd{font-size:1.3rem;font-weight:800;font-variant-numeric:tabular-nums}
/* compact bar and vertical steps */
.st-demo-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:34px;align-items:start}
.st-compact p{display:flex;justify-content:space-between;gap:12px;font-size:.88rem;margin-bottom:10px}
.st-compact p span{color:var(--st-muted)}
.st-bars{list-style:none;display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.st-bars li{height:8px;border-radius:99px;background:var(--st-line)}
.st-bars li[data-state="done"]{background:var(--st-accent)}
.st-bars li[data-state="current"]{background:linear-gradient(90deg,var(--st-accent) 50%,#d9c3cd 50%)}
.st-vert{list-style:none;display:grid}
.st-vert li{position:relative;display:grid;grid-template-columns:32px minmax(0,1fr);gap:14px;padding-bottom:20px;color:var(--st-muted)}
.st-vert li:last-child{padding-bottom:0}
.st-vert li::before{content:"";position:absolute;left:15px;top:34px;bottom:2px;width:2px;background:var(--st-line)}
.st-vert li:last-child::before{display:none}
.st-vert li[data-state="done"]::before{background:var(--st-accent)}
.st-vert strong{display:block;color:var(--st-ink);font-size:.93rem;margin-top:5px}
.st-vert small{font-size:.8rem}
.st-vert li[data-state="current"] .st-dot{border-color:var(--st-accent);color:var(--st-accent);box-shadow:0 0 0 4px var(--st-ring)}
.st-vert li[data-state="done"] .st-dot{border-color:var(--st-accent);background:var(--st-accent);color:#fff}
.st-vert li[data-state="done"] .st-dot span{display:none}
.st-vert li[data-state="done"] .st-dot svg{display:block}
@media (max-width:900px){.st-layout{grid-template-columns:1fr}.st-sum{position:static}}
@media (max-width:575px){.st-card{padding:22px 18px}.st-m6{grid-column:span 6}.st-m3{grid-column:span 3}.st-step{font-size:.72rem}.st-review div{grid-template-columns:1fr;gap:2px}}
@media (prefers-reduced-motion:reduce){.st-input,.st-radio,.st-btn{transition:none}}
JavaScript
// Checkout form (demo): nothing is sent and no card is charged. On submit every field is checked with the
// browser's own validity rules; a failing field gets aria-invalid and the message from its data-missing or
// data-bad attribute in the element with id "<field id>-err" (already in its aria-describedby), and focus
// moves to the first problem. [data-ship-option] radios update shipping and the total in the summary.
document.querySelectorAll('form[data-checkout]').forEach(function (form) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var fmt = function (c) { return money.format(c / 100); };
  var rate = parseFloat(form.dataset.tax || 0), status = form.querySelector('[data-status]');
  function put(sel, text) { form.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = cents(form.dataset.subtotal), opt = form.querySelector('[data-ship-option]:checked');
    var ship = opt ? cents(opt.dataset.price) : 0, tax = Math.round(sub * rate / 100), total = sub + ship + tax;
    put('[data-subtotal]', fmt(sub)); put('[data-tax]', fmt(tax)); put('[data-total]', fmt(total));
    put('[data-shipping]', ship ? fmt(ship) : 'Free');
    if (opt) put('[data-ship-label]', opt.dataset.label);
    return total;
  }
  function check(field) {
    var v = field.validity, msg = v.valid ? '' : v.valueMissing ? field.dataset.missing : (field.dataset.bad || field.validationMessage);
    var out = document.getElementById(field.id + '-err');
    field.classList.toggle('is-invalid', !!msg);
    if (msg) field.setAttribute('aria-invalid', 'true'); else field.removeAttribute('aria-invalid');
    if (out) out.textContent = msg || '';
    return !msg;
  }
  form.noValidate = true;
  form.addEventListener('change', function (e) {
    if (!e.target.matches('[data-ship-option]')) return;
    var total = render();
    if (status) status.textContent = e.target.dataset.label + ' selected. Total ' + fmt(total) + '.';
  });
  form.addEventListener('input', function (e) { if (e.target.hasAttribute('aria-invalid')) check(e.target); });
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // demo only: nothing is sent anywhere
    var bad = Array.prototype.filter.call(form.elements, function (f) { return f.willValidate && f.id && !check(f); });
    if (bad.length) { if (status) status.textContent = ''; bad[0].focus(); return; }
    if (status) status.textContent = 'Demo order placed. Nothing was charged: this form is not connected to a payment provider.';
  });
  render();
});

// Multi-step checkout: one form split into [data-step-panel] sections. Continue checks only the fields in the
// current step, then shows the next one, moves aria-current="step" in the progress list and puts focus on the
// new step's heading. Without the script every step stays visible and the form works as one long page.
document.querySelectorAll('form[data-steps]').forEach(function (form) {
  var panels = [].slice.call(form.querySelectorAll('[data-step-panel]'));
  var marks = [].slice.call(form.querySelectorAll('[data-step-mark]'));
  var at = 0;
  function check(field) {
    var v = field.validity, msg = v.valid ? '' : v.valueMissing ? field.dataset.missing : (field.dataset.bad || field.validationMessage);
    var out = document.getElementById(field.id + '-err');
    field.classList.toggle('is-invalid', !!msg);
    if (msg) field.setAttribute('aria-invalid', 'true'); else field.removeAttribute('aria-invalid');
    if (out) out.textContent = msg || '';
    return !msg;
  }
  function show(n, focus) {
    at = n;
    panels.forEach(function (p, i) { p.hidden = i !== n; });
    marks.forEach(function (m, i) {
      if (i === n) m.setAttribute('aria-current', 'step'); else m.removeAttribute('aria-current');
      m.dataset.state = i < n ? 'done' : i === n ? 'current' : 'next';
      m.querySelector('[data-step-sr]').textContent = i < n ? ', completed' : '';
    });
    form.querySelectorAll('[data-review]').forEach(function (el) {
      var f = document.getElementById(el.dataset.review);
      el.textContent = f && f.value.trim() ? f.value.trim() : 'Not given';
    });
    form.querySelectorAll('[data-review-card]').forEach(function (el) {
      var digits = document.getElementById(el.dataset.reviewCard).value.replace(/\D/g, '');
      el.textContent = digits ? 'Card ending ' + digits.slice(-4) : 'Not given';
    });
    if (focus) panels[n].querySelector('[tabindex="-1"]').focus();
  }
  form.querySelectorAll('[data-next],[data-prev]').forEach(function (b) { b.hidden = false; });
  form.addEventListener('click', function (e) {
    if (e.target.closest('[data-prev]')) return show(at - 1, true);
    if (!e.target.closest('[data-next]')) return;
    var bad = [].filter.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return f.willValidate && f.id && !check(f); });
    if (bad.length) bad[0].focus(); else show(at + 1, true);
  });
  show(0, false);
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Numbered stepper checkout</span>
<form class="st" action="#" method="post" data-checkout data-steps data-subtotal="281.00" aria-label="Checkout">
  <div class="st-layout">
    <div class="card"><div class="card-body">
      <p class="st-brand">Marlow <em>&</em> Fine</p>
      <nav aria-label="Checkout progress"><ol class="st-steps">
        <li class="st-step" data-step-mark data-state="current" aria-current="step"><span class="st-dot" aria-hidden="true"><span>1</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Details<span class="visually-hidden" data-step-sr></span></li>
        <li class="st-step" data-step-mark data-state="next"><span class="st-dot" aria-hidden="true"><span>2</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Delivery<span class="visually-hidden" data-step-sr></span></li>
        <li class="st-step" data-step-mark data-state="next"><span class="st-dot" aria-hidden="true"><span>3</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Payment<span class="visually-hidden" data-step-sr></span></li>
        <li class="st-step" data-step-mark data-state="next"><span class="st-dot" aria-hidden="true"><span>4</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Review<span class="visually-hidden" data-step-sr></span></li>
      </ol></nav>
        <section class="st-panel" data-step-panel aria-labelledby="st-h1">
          <h2 class="st-h" id="st-h1" tabindex="-1">Your details</h2>
          <p class="st-sub">Where should we send your order?</p>
          <div class="row">
            <div class="col-12 col-sm-12"><label class="form-label" for="st-email">Email</label><input class="form-control" id="st-email" name="email" type="email" autocomplete="email" required aria-describedby="st-email-hint st-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected]."><p class="form-text" id="st-email-hint">For your receipt and tracking link.</p><p class="invalid-feedback" id="st-email-err"></p></div>
            <div class="col-12 col-sm-12"><label class="form-label" for="st-name">Full name</label><input class="form-control" id="st-name" name="name" type="text" autocomplete="shipping name" required aria-describedby="st-name-err" data-missing="Enter your full name."><p class="invalid-feedback" id="st-name-err"></p></div>
            <div class="col-12 col-sm-12"><label class="form-label" for="st-address">Street address</label><input class="form-control" id="st-address" name="address" type="text" autocomplete="shipping address-line1" required aria-describedby="st-address-err" data-missing="Enter your street address."><p class="invalid-feedback" id="st-address-err"></p></div>
            <div class="col-12 col-sm-4"><label class="form-label" for="st-city">City</label><input class="form-control" id="st-city" name="city" type="text" autocomplete="shipping address-level2" required aria-describedby="st-city-err" data-missing="Enter your city."><p class="invalid-feedback" id="st-city-err"></p></div>
            <div class="col-6 col-sm-4"><label class="form-label" for="st-state">State</label><input class="form-control" id="st-state" name="state" type="text" autocomplete="shipping address-level1" required aria-describedby="st-state-err" data-missing="Enter your state."><p class="invalid-feedback" id="st-state-err"></p></div>
            <div class="col-6 col-sm-4"><label class="form-label" for="st-zip">ZIP code</label><input class="form-control" id="st-zip" name="zip" type="text" autocomplete="shipping postal-code" inputmode="numeric" pattern="[0-9]{5}(-[0-9]{4})?" required aria-describedby="st-zip-err" data-missing="Enter your ZIP code." data-bad="Enter a 5-digit ZIP code, like 02118."><p class="invalid-feedback" id="st-zip-err"></p></div>
          </div>
          <div class="st-nav"><button class="btn btn-primary" type="button" data-next hidden>Continue to delivery</button></div>
        </section>
        <section class="st-panel" data-step-panel aria-labelledby="st-h2">
          <h2 class="st-h" id="st-h2" tabindex="-1">Delivery</h2>
          <p class="st-sub">Every parcel is insured and needs a signature.</p>
          <fieldset class="st-options"><legend class="visually-hidden">Delivery method</legend>
            <label class="form-check st-radio"><input class="form-check-input" type="radio" name="delivery" value="std" data-ship-option data-label="Insured standard" data-price="8.00" checked><span><strong>Insured standard</strong><span>3 to 5 business days, signature on delivery</span></span><b>$8.00</b></label>
            <label class="form-check st-radio"><input class="form-check-input" type="radio" name="delivery" value="exp" data-ship-option data-label="Insured express" data-price="18.00"><span><strong>Insured express</strong><span>Next business day, signature on delivery</span></span><b>$18.00</b></label>
            <label class="form-check st-radio"><input class="form-check-input" type="radio" name="delivery" value="collect" data-ship-option data-label="Collect from the studio" data-price="0.00"><span><strong>Collect from the studio</strong><span>Ready tomorrow · 4 Wharf Lane</span></span><b>Free</b></label>
          </fieldset>
          <div class="st-nav"><button class="btn btn-outline-primary" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="btn btn-primary" type="button" data-next hidden>Continue to payment</button></div>
        </section>
        <section class="st-panel" data-step-panel aria-labelledby="st-h3">
          <h2 class="st-h" id="st-h3" tabindex="-1">Payment</h2>
          <p class="st-sub">Card details for this demo checkout.</p>
          <div class="alert"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8-8 9-4.6-1-8-4.5-8-9V6z"/><path d="M12 8v4M12 16h.01"/></svg><p><strong>Demo payment form</strong>Not connected to a payment provider. Nothing you type is sent or stored and no card is charged.</p></div>
          <div class="row">
            <div class="col-12 col-sm-12"><label class="form-label" for="st-card">Card number</label><input class="form-control" id="st-card" type="text" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]{13,23}" placeholder="1234 1234 1234 1234" required aria-describedby="st-card-err" data-missing="Enter a card number." data-bad="Enter the 13 to 19 digits on the front of the card."><p class="invalid-feedback" id="st-card-err"></p></div>
            <div class="col-6 col-sm-6"><label class="form-label" for="st-exp">Expiry date</label><input class="form-control" id="st-exp" type="text" autocomplete="cc-exp" inputmode="numeric" pattern="(0[1-9]|1[0-2]) ?/ ?[0-9]{2}" placeholder="MM / YY" required aria-describedby="st-exp-err" data-missing="Enter the expiry date." data-bad="Use the format MM / YY."><p class="invalid-feedback" id="st-exp-err"></p></div>
            <div class="col-6 col-sm-6"><label class="form-label" for="st-cvc">Security code</label><input class="form-control" id="st-cvc" type="text" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]{3,4}" required aria-describedby="st-cvc-hint st-cvc-err" data-missing="Enter the security code." data-bad="Enter the 3 or 4 digit code."><p class="form-text" id="st-cvc-hint">3 or 4 digits on the back.</p><p class="invalid-feedback" id="st-cvc-err"></p></div>
            <div class="col-12 col-sm-12"><label class="form-label" for="st-ccname">Name on card</label><input class="form-control" id="st-ccname" type="text" autocomplete="cc-name" required aria-describedby="st-ccname-err" data-missing="Enter the name on the card."><p class="invalid-feedback" id="st-ccname-err"></p></div>
          </div>
          <div class="st-nav"><button class="btn btn-outline-primary" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="btn btn-primary" type="button" data-next hidden>Continue to review</button></div>
        </section>
        <section class="st-panel" data-step-panel aria-labelledby="st-h4">
          <h2 class="st-h" id="st-h4" tabindex="-1">Review your order</h2>
          <p class="st-sub">Check everything before you place the order.</p>
          <dl class="st-review">
            <div><dt>Email</dt><dd><span data-review="st-email">Not given</span></dd></div>
            <div><dt>Ship to</dt><dd><span data-review="st-name">Not given</span>, <span data-review="st-address">Not given</span>, <span data-review="st-city">Not given</span> <span data-review="st-zip">Not given</span></dd></div>
            <div><dt>Delivery</dt><dd><span data-ship-label>Insured standard</span></dd></div>
            <div><dt>Payment</dt><dd><span data-review-card="st-card">Not given</span> (demo)</dd></div>
          </dl>
          <p class="st-status" role="status" data-status></p>
          <div class="st-nav"><button class="btn btn-outline-primary" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="btn btn-primary" type="submit"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>Place demo order</button></div>
        </section>
    </div></div>
    <aside class="card st-sum" aria-labelledby="st-sum-title"><div class="card-body">
      <h2 id="st-sum-title">Order summary</h2>
      <ul class="list-group">
        <li class="list-group-item st-item"><img src="https://images.unsplash.com/photo-1603561591411-07134e71a2a9?w=104&h=104&fit=crop&auto=format&q=70" alt="" width="52" height="52"><div><p>Pink tourmaline ring</p><p class="st-meta">Rose gold · Size 6 · Qty 1</p></div><p class="st-price">$185.00</p></li>
        <li class="list-group-item st-item"><img src="https://images.unsplash.com/photo-1535632066927-ab7c9ab60908?w=104&h=104&fit=crop&auto=format&q=70" alt="" width="52" height="52"><div><p>Blue topaz drop earrings</p><p class="st-meta">Sterling silver · Qty 1</p></div><p class="st-price">$96.00</p></li>
      </ul>
      <dl class="st-rows">
        <div class="st-row"><dt>Subtotal</dt><dd data-subtotal>$281.00</dd></div>
        <div class="st-row"><dt>Delivery</dt><dd data-shipping>$8.00</dd></div>
      </dl>
      <dl class="st-total"><dt>Total</dt><dd data-total>$289.00</dd></dl>
    </div></aside>
  </div>
</form>
</div>
<div class="st st-demo-row">
<div><span class="lbl">Compact progress bar</span>
<nav class="card" aria-label="Checkout progress, compact"><div class="card-body st-compact">
  <p><strong>Step 2 of 4: Delivery</strong><span>Next: Payment</span></p>
  <ol class="st-bars">
    <li data-state="done"><span class="visually-hidden">Details, completed</span></li>
    <li data-state="current" aria-current="step"><span class="visually-hidden">Delivery</span></li>
    <li data-state="next"><span class="visually-hidden">Payment</span></li>
    <li data-state="next"><span class="visually-hidden">Review</span></li>
  </ol>
</div></nav>
</div>
<div><span class="lbl">Vertical steps</span>
<nav class="card" aria-label="Checkout progress, vertical"><div class="card-body">
  <ol class="st-vert">
    <li data-state="done"><span class="st-dot" aria-hidden="true"><span>1</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Details<span class="visually-hidden">, completed</span></strong><small>[email protected]</small></div></li>
    <li data-state="done"><span class="st-dot" aria-hidden="true"><span>2</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Delivery<span class="visually-hidden">, completed</span></strong><small>Insured express, next business day</small></div></li>
    <li data-state="current" aria-current="step"><span class="st-dot" aria-hidden="true"><span>3</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Payment</strong><small>Card details</small></div></li>
    <li data-state="next"><span class="st-dot" aria-hidden="true"><span>4</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong>Review</strong><small>Check and place the order</small></div></li>
  </ol>
</div></nav>
</div>
</div>
CSS
/* Bootstrap .card, .row.g-3, .form-control, .form-check, .invalid-feedback, .alert, .list-group and
   .btn, recoloured through Bootstrap's CSS variables; the step indicator is a small custom list with
   aria-current="step". Card fields are a front-end mock with no name attribute, and the demo submit is stopped. */
.st{--st-accent:#7a2e4f;--st-accent-dark:#5f2240;--st-ink:#22161c;--st-muted:#625560;--st-line:#ebe3e7;--st-field:#8a7d85;--st-soft:#faf5f7;
  --st-ring:rgba(122,46,79,.25);--st-error:#b42318;--st-ok:#166534;color:var(--st-ink);font-size:.93rem;line-height:1.45}
.st :is(a,button):focus-visible,.st-h:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.st-h:focus:not(:focus-visible){outline:none}
.st-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:28px;align-items:start}
.st-brand{font-family:Georgia,"Times New Roman",serif;font-size:1.3rem;margin-bottom:20px}
.st-brand em{color:var(--st-accent)}
/* numbered step indicator */
.st-steps{list-style:none;display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:26px;counter-reset:none}
.st-step{position:relative;display:grid;justify-items:center;gap:6px;text-align:center;font-size:.8rem;font-weight:600;color:var(--st-muted)}
.st-step::before{content:"";position:absolute;top:15px;right:50%;left:-50%;height:2px;background:var(--st-line)}
.st-step:first-child::before{display:none}
.st-step[data-state="done"]::before,.st-step[data-state="current"]::before{background:var(--st-accent)}
.st-dot{position:relative;z-index:1;width:32px;height:32px;border-radius:50%;border:2px solid var(--st-field);background:#fff;display:grid;place-items:center;font-size:.82rem}
.st-dot svg{display:none}
.st-step[data-state="current"]{color:var(--st-ink)}
.st-step[data-state="current"] .st-dot{border-color:var(--st-accent);color:var(--st-accent);box-shadow:0 0 0 4px var(--st-ring)}
.st-step[data-state="done"] .st-dot{border-color:var(--st-accent);background:var(--st-accent);color:#fff}
.st-step[data-state="done"] .st-dot span{display:none}
.st-step[data-state="done"] .st-dot svg{display:block}
.st-panel:not([hidden])+.st-panel{margin-top:28px;padding-top:24px;border-top:1px solid var(--st-line)}
.st-h{font-size:1.2rem;font-weight:700;margin-bottom:4px}
.st-sub{color:var(--st-muted);margin-bottom:18px}
.st-options{border:0;padding:0;margin:0;display:grid;gap:10px}
.st-radio{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:start;padding:14px 16px;border:1px solid var(--st-field);border-radius:12px;cursor:pointer;transition:border-color .15s,background-color .15s}
.st-radio:hover{border-color:var(--st-ink)}
.st-radio:has(:checked){border-color:var(--st-accent);background:var(--st-soft);box-shadow:inset 0 0 0 1px var(--st-accent)}
.st-radio strong{display:block;font-weight:600}
.st-radio span span{font-size:.82rem;color:var(--st-muted)}
.st-radio b{font-variant-numeric:tabular-nums}
.st-review{display:grid;gap:0;border:1px solid var(--st-line);border-radius:12px}
.st-review div{display:grid;grid-template-columns:110px minmax(0,1fr);gap:12px;padding:12px 16px}
.st-review div+div{border-top:1px solid var(--st-line)}
.st-review dt{color:var(--st-muted);font-size:.84rem}
.st-review dd{font-weight:600;overflow-wrap:anywhere}
.st-nav{display:flex;justify-content:space-between;gap:12px;margin-top:22px}
  font-weight:700;cursor:pointer;transition:background-color .15s;margin-left:auto}
.st-status:not(:empty){margin-top:14px;padding:12px 14px;border-radius:10px;background:#f0fdf4;color:var(--st-ok);font-weight:600;font-size:.85rem}
/* summary */
.st-sum{position:sticky;top:20px}
.st-sum h2{font-size:1.05rem;font-weight:700;margin-bottom:6px}
.st-items{list-style:none;border-bottom:1px solid var(--st-line);padding-bottom:6px}
.st-item{display:grid;grid-template-columns:52px minmax(0,1fr) auto;gap:12px;align-items:center;padding:8px 0}
.st-item img{width:52px;height:52px;border-radius:10px;object-fit:cover;background:var(--st-soft)}
.st-item p:first-child{font-weight:600;font-size:.88rem;line-height:1.3}
.st-meta{font-size:.78rem;color:var(--st-muted)}
.st-price{font-weight:600;font-variant-numeric:tabular-nums}
.st-rows{display:grid;gap:8px;padding:14px 0;border-bottom:1px solid var(--st-line)}
.st-row{display:flex;justify-content:space-between;gap:12px}
.st-row dt{color:var(--st-muted)}
.st-row dd{font-weight:600;font-variant-numeric:tabular-nums}
.st-total{display:flex;justify-content:space-between;align-items:baseline;padding-top:14px;font-weight:700}
.st-total dd{font-size:1.3rem;font-weight:800;font-variant-numeric:tabular-nums}
/* compact bar and vertical steps */
.st-demo-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:34px;align-items:start}
.st-compact p{display:flex;justify-content:space-between;gap:12px;font-size:.88rem;margin-bottom:10px}
.st-compact p span{color:var(--st-muted)}
.st-bars{list-style:none;display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.st-bars li{height:8px;border-radius:99px;background:var(--st-line)}
.st-bars li[data-state="done"]{background:var(--st-accent)}
.st-bars li[data-state="current"]{background:linear-gradient(90deg,var(--st-accent) 50%,#d9c3cd 50%)}
.st-vert{list-style:none;display:grid}
.st-vert li{position:relative;display:grid;grid-template-columns:32px minmax(0,1fr);gap:14px;padding-bottom:20px;color:var(--st-muted)}
.st-vert li:last-child{padding-bottom:0}
.st-vert li::before{content:"";position:absolute;left:15px;top:34px;bottom:2px;width:2px;background:var(--st-line)}
.st-vert li:last-child::before{display:none}
.st-vert li[data-state="done"]::before{background:var(--st-accent)}
.st-vert strong{display:block;color:var(--st-ink);font-size:.93rem;margin-top:5px}
.st-vert small{font-size:.8rem}
.st-vert li[data-state="current"] .st-dot{border-color:var(--st-accent);color:var(--st-accent);box-shadow:0 0 0 4px var(--st-ring)}
.st-vert li[data-state="done"] .st-dot{border-color:var(--st-accent);background:var(--st-accent);color:#fff}
.st-vert li[data-state="done"] .st-dot span{display:none}
.st-vert li[data-state="done"] .st-dot svg{display:block}
@media (max-width:900px){.st-layout{grid-template-columns:1fr}.st-sum{position:static}}
@media (max-width:575px){.st .card{--bs-card-spacer-x:18px;--bs-card-spacer-y:22px}.st-step{font-size:.72rem}.st-review div{grid-template-columns:1fr;gap:2px}}
@media (prefers-reduced-motion:reduce){.st .form-control,.st-radio,.st .btn{transition:none}}

.st{--bs-body-color:var(--st-ink);--bs-secondary-color:var(--st-muted);--bs-border-color:var(--st-field);--bs-border-radius:10px;
  --bs-form-invalid-border-color:var(--st-error);--bs-form-invalid-color:var(--st-error)}
:where(.st) :is(p,h2,dl,dd,ol,ul){margin:0}
.st .card{--bs-card-border-width:0;--bs-card-border-radius:18px;--bs-card-spacer-x:28px;--bs-card-spacer-y:28px;box-shadow:0 1px 2px rgba(34,22,28,.05),0 16px 40px -24px rgba(34,22,28,.32)}
.st .card.st-sum{--bs-card-spacer-x:22px;--bs-card-spacer-y:22px}
.st .st-steps,.st .st-vert{padding:0}
.st .row{--bs-gutter-x:14px;--bs-gutter-y:14px}
.st .form-label{font-size:.84rem;font-weight:600;margin-bottom:6px}
.st .form-control{padding:11px 13px;font-size:inherit;line-height:inherit;color:var(--st-ink);transition:border-color .15s,box-shadow .15s}
.st .form-control::placeholder{color:#7f727a}
.st .form-control:focus{border-color:var(--st-accent);box-shadow:0 0 0 3px var(--st-ring)}
.st .form-control.is-invalid{background-image:none;padding-right:13px}
.st .form-control.is-invalid:focus{box-shadow:0 0 0 3px rgba(180,35,24,.2)}
.st .form-text{font-size:.78rem;color:var(--st-muted);margin-top:5px}
.st .invalid-feedback{display:flex;gap:6px;font-size:.8rem;font-weight:600;margin-top:6px}
.st .invalid-feedback::before{content:"!";flex:none;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--st-error);color:#fff;font-size:.7rem;font-weight:800;display:grid;place-items:center}
.st .invalid-feedback:empty{display:none}
.st-radio.form-check{display:grid;padding:14px 16px;margin:0;min-height:0}
.st-radio .form-check-input{float:none;margin:1px 0 0;border-color:var(--st-field)}
.st-radio .form-check-input:checked{background-color:var(--st-accent);border-color:var(--st-accent)}
.st-radio .form-check-input:focus{box-shadow:none;border-color:var(--st-field)}
.st-radio .form-check-input:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.st .alert{--bs-alert-bg:#fff7e6;--bs-alert-border-color:#e7c98f;--bs-alert-color:#5c3b06;--bs-alert-padding-x:16px;--bs-alert-padding-y:14px;--bs-alert-border-radius:12px;--bs-alert-margin-bottom:16px;
  display:flex;gap:12px;font-size:.85rem}
.st .alert svg{flex:none;margin-top:1px}
.st .alert strong{display:block}
.st-review dt,.st-row dt{font-weight:400}
.st .btn{--bs-btn-padding-x:20px;--bs-btn-padding-y:11px;--bs-btn-font-weight:700;--bs-btn-border-radius:999px;display:inline-flex;align-items:center;gap:8px}
.st .btn-primary{--bs-btn-bg:var(--st-accent);--bs-btn-border-color:var(--st-accent);--bs-btn-hover-bg:var(--st-accent-dark);--bs-btn-hover-border-color:var(--st-accent-dark);
  --bs-btn-active-bg:var(--st-accent-dark);--bs-btn-active-border-color:var(--st-accent-dark);margin-left:auto}
.st .btn-outline-primary{--bs-btn-color:var(--st-accent);--bs-btn-border-color:var(--st-accent);--bs-btn-hover-bg:var(--st-soft);--bs-btn-hover-color:var(--st-accent);
  --bs-btn-hover-border-color:var(--st-accent);--bs-btn-active-bg:var(--st-line);--bs-btn-active-color:var(--st-accent);--bs-btn-active-border-color:var(--st-accent)}
.st .btn:focus-visible{box-shadow:none}
.st .list-group{--bs-list-group-border-width:0;--bs-list-group-item-padding-x:0;--bs-list-group-item-padding-y:8px;--bs-list-group-bg:transparent;--bs-list-group-color:var(--st-ink);border-bottom:1px solid var(--st-line);padding-bottom:6px;border-radius:0}
.st-item.list-group-item{display:grid}
JavaScript
// Checkout form (demo): nothing is sent and no card is charged. On submit every field is checked with the
// browser's own validity rules; a failing field gets aria-invalid and the message from its data-missing or
// data-bad attribute in the element with id "<field id>-err" (already in its aria-describedby), and focus
// moves to the first problem. [data-ship-option] radios update shipping and the total in the summary.
document.querySelectorAll('form[data-checkout]').forEach(function (form) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var fmt = function (c) { return money.format(c / 100); };
  var rate = parseFloat(form.dataset.tax || 0), status = form.querySelector('[data-status]');
  function put(sel, text) { form.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = cents(form.dataset.subtotal), opt = form.querySelector('[data-ship-option]:checked');
    var ship = opt ? cents(opt.dataset.price) : 0, tax = Math.round(sub * rate / 100), total = sub + ship + tax;
    put('[data-subtotal]', fmt(sub)); put('[data-tax]', fmt(tax)); put('[data-total]', fmt(total));
    put('[data-shipping]', ship ? fmt(ship) : 'Free');
    if (opt) put('[data-ship-label]', opt.dataset.label);
    return total;
  }
  function check(field) {
    var v = field.validity, msg = v.valid ? '' : v.valueMissing ? field.dataset.missing : (field.dataset.bad || field.validationMessage);
    var out = document.getElementById(field.id + '-err');
    field.classList.toggle('is-invalid', !!msg);
    if (msg) field.setAttribute('aria-invalid', 'true'); else field.removeAttribute('aria-invalid');
    if (out) out.textContent = msg || '';
    return !msg;
  }
  form.noValidate = true;
  form.addEventListener('change', function (e) {
    if (!e.target.matches('[data-ship-option]')) return;
    var total = render();
    if (status) status.textContent = e.target.dataset.label + ' selected. Total ' + fmt(total) + '.';
  });
  form.addEventListener('input', function (e) { if (e.target.hasAttribute('aria-invalid')) check(e.target); });
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // demo only: nothing is sent anywhere
    var bad = Array.prototype.filter.call(form.elements, function (f) { return f.willValidate && f.id && !check(f); });
    if (bad.length) { if (status) status.textContent = ''; bad[0].focus(); return; }
    if (status) status.textContent = 'Demo order placed. Nothing was charged: this form is not connected to a payment provider.';
  });
  render();
});

// Multi-step checkout: one form split into [data-step-panel] sections. Continue checks only the fields in the
// current step, then shows the next one, moves aria-current="step" in the progress list and puts focus on the
// new step's heading. Without the script every step stays visible and the form works as one long page.
document.querySelectorAll('form[data-steps]').forEach(function (form) {
  var panels = [].slice.call(form.querySelectorAll('[data-step-panel]'));
  var marks = [].slice.call(form.querySelectorAll('[data-step-mark]'));
  var at = 0;
  function check(field) {
    var v = field.validity, msg = v.valid ? '' : v.valueMissing ? field.dataset.missing : (field.dataset.bad || field.validationMessage);
    var out = document.getElementById(field.id + '-err');
    field.classList.toggle('is-invalid', !!msg);
    if (msg) field.setAttribute('aria-invalid', 'true'); else field.removeAttribute('aria-invalid');
    if (out) out.textContent = msg || '';
    return !msg;
  }
  function show(n, focus) {
    at = n;
    panels.forEach(function (p, i) { p.hidden = i !== n; });
    marks.forEach(function (m, i) {
      if (i === n) m.setAttribute('aria-current', 'step'); else m.removeAttribute('aria-current');
      m.dataset.state = i < n ? 'done' : i === n ? 'current' : 'next';
      m.querySelector('[data-step-sr]').textContent = i < n ? ', completed' : '';
    });
    form.querySelectorAll('[data-review]').forEach(function (el) {
      var f = document.getElementById(el.dataset.review);
      el.textContent = f && f.value.trim() ? f.value.trim() : 'Not given';
    });
    form.querySelectorAll('[data-review-card]').forEach(function (el) {
      var digits = document.getElementById(el.dataset.reviewCard).value.replace(/\D/g, '');
      el.textContent = digits ? 'Card ending ' + digits.slice(-4) : 'Not given';
    });
    if (focus) panels[n].querySelector('[tabindex="-1"]').focus();
  }
  form.querySelectorAll('[data-next],[data-prev]').forEach(function (b) { b.hidden = false; });
  form.addEventListener('click', function (e) {
    if (e.target.closest('[data-prev]')) return show(at - 1, true);
    if (!e.target.closest('[data-next]')) return;
    var bad = [].filter.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return f.willValidate && f.id && !check(f); });
    if (bad.length) bad[0].focus(); else show(at + 1, true);
  });
  show(0, false);
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#625560] uppercase">Numbered stepper checkout</span>
<form class="text-[.93rem] leading-[1.45] text-st-ink" action="#" method="post" data-checkout data-steps data-subtotal="281.00" aria-label="Checkout">
  <div class="grid grid-cols-[minmax(0,1fr)_320px] items-start gap-7 max-[900px]:grid-cols-1">
    <div class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(34,22,28,.05),0_16px_40px_-24px_rgba(34,22,28,.32)] p-7 max-[575px]:px-[18px] max-[575px]:py-[22px]">
      <p class="mb-5 font-[Georgia,'Times_New_Roman',serif] text-[1.3rem]">Marlow <em class="text-st-accent">&</em> Fine</p>
      <nav aria-label="Checkout progress"><ol class="mb-[26px] grid grid-cols-4">
        <li class="group relative grid justify-items-center gap-1.5 text-center text-[.8rem] font-semibold text-st-muted data-[state=current]:text-st-ink max-[575px]:text-[.72rem] before:absolute before:top-[15px] before:right-1/2 before:-left-1/2 before:h-0.5 before:bg-st-line first:before:hidden data-[state=current]:before:bg-st-accent data-[state=done]:before:bg-st-accent" data-step-mark data-state="current" aria-current="step"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>1</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Details<span class="sr-only" data-step-sr></span></li>
        <li class="group relative grid justify-items-center gap-1.5 text-center text-[.8rem] font-semibold text-st-muted data-[state=current]:text-st-ink max-[575px]:text-[.72rem] before:absolute before:top-[15px] before:right-1/2 before:-left-1/2 before:h-0.5 before:bg-st-line first:before:hidden data-[state=current]:before:bg-st-accent data-[state=done]:before:bg-st-accent" data-step-mark data-state="next"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>2</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Delivery<span class="sr-only" data-step-sr></span></li>
        <li class="group relative grid justify-items-center gap-1.5 text-center text-[.8rem] font-semibold text-st-muted data-[state=current]:text-st-ink max-[575px]:text-[.72rem] before:absolute before:top-[15px] before:right-1/2 before:-left-1/2 before:h-0.5 before:bg-st-line first:before:hidden data-[state=current]:before:bg-st-accent data-[state=done]:before:bg-st-accent" data-step-mark data-state="next"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>3</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Payment<span class="sr-only" data-step-sr></span></li>
        <li class="group relative grid justify-items-center gap-1.5 text-center text-[.8rem] font-semibold text-st-muted data-[state=current]:text-st-ink max-[575px]:text-[.72rem] before:absolute before:top-[15px] before:right-1/2 before:-left-1/2 before:h-0.5 before:bg-st-line first:before:hidden data-[state=current]:before:bg-st-accent data-[state=done]:before:bg-st-accent" data-step-mark data-state="next"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>4</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>Review<span class="sr-only" data-step-sr></span></li>
      </ol></nav>
        <section class="[&:not([hidden])+&]:mt-7 [&:not([hidden])+&]:border-t [&:not([hidden])+&]:border-st-line [&:not([hidden])+&]:pt-6" data-step-panel aria-labelledby="st-h1">
          <h2 class="mb-1 text-[1.2rem] font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" id="st-h1" tabindex="-1">Your details</h2>
          <p class="mb-[18px] text-st-muted">Where should we send your order?</p>
          <div class="grid grid-cols-6 gap-3.5">
            <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-email">Email</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-email" name="email" type="email" autocomplete="email" required aria-describedby="st-email-hint st-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected]."><p class="mt-[5px] text-[.78rem] text-st-muted" id="st-email-hint">For your receipt and tracking link.</p><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-email-err"></p></div>
            <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-name">Full name</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-name" name="name" type="text" autocomplete="shipping name" required aria-describedby="st-name-err" data-missing="Enter your full name."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-name-err"></p></div>
            <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-address">Street address</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-address" name="address" type="text" autocomplete="shipping address-line1" required aria-describedby="st-address-err" data-missing="Enter your street address."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-address-err"></p></div>
            <div class="col-span-6 min-[576px]:col-span-2"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-city">City</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-city" name="city" type="text" autocomplete="shipping address-level2" required aria-describedby="st-city-err" data-missing="Enter your city."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-city-err"></p></div>
            <div class="col-span-3 min-[576px]:col-span-2"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-state">State</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-state" name="state" type="text" autocomplete="shipping address-level1" required aria-describedby="st-state-err" data-missing="Enter your state."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-state-err"></p></div>
            <div class="col-span-3 min-[576px]:col-span-2"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-zip">ZIP code</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-zip" name="zip" type="text" autocomplete="shipping postal-code" inputmode="numeric" pattern="[0-9]{5}(-[0-9]{4})?" required aria-describedby="st-zip-err" data-missing="Enter your ZIP code." data-bad="Enter a 5-digit ZIP code, like 02118."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-zip-err"></p></div>
          </div>
          <div class="mt-[22px] flex justify-between gap-3"><button class="ml-auto inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-st-accent px-5 py-3 font-bold text-white transition-colors duration-150 hover:bg-st-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="button" data-next hidden>Continue to delivery</button></div>
        </section>
        <section class="[&:not([hidden])+&]:mt-7 [&:not([hidden])+&]:border-t [&:not([hidden])+&]:border-st-line [&:not([hidden])+&]:pt-6" data-step-panel aria-labelledby="st-h2">
          <h2 class="mb-1 text-[1.2rem] font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" id="st-h2" tabindex="-1">Delivery</h2>
          <p class="mb-[18px] text-st-muted">Every parcel is insured and needs a signature.</p>
          <fieldset class="grid gap-2.5"><legend class="sr-only">Delivery method</legend>
            <label class="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-xl border border-st-field px-4 py-3.5 transition-colors duration-150 hover:border-st-ink has-checked:border-st-accent has-checked:bg-st-soft has-checked:shadow-[inset_0_0_0_1px_var(--color-st-accent)] motion-reduce:transition-none"><input class="mt-px size-[18px] cursor-pointer accent-st-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="radio" name="delivery" value="std" data-ship-option data-label="Insured standard" data-price="8.00" checked><span><strong class="block font-semibold">Insured standard</strong><span class="text-[.82rem] text-st-muted">3 to 5 business days, signature on delivery</span></span><b class="tabular-nums">$8.00</b></label>
            <label class="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-xl border border-st-field px-4 py-3.5 transition-colors duration-150 hover:border-st-ink has-checked:border-st-accent has-checked:bg-st-soft has-checked:shadow-[inset_0_0_0_1px_var(--color-st-accent)] motion-reduce:transition-none"><input class="mt-px size-[18px] cursor-pointer accent-st-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="radio" name="delivery" value="exp" data-ship-option data-label="Insured express" data-price="18.00"><span><strong class="block font-semibold">Insured express</strong><span class="text-[.82rem] text-st-muted">Next business day, signature on delivery</span></span><b class="tabular-nums">$18.00</b></label>
            <label class="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-xl border border-st-field px-4 py-3.5 transition-colors duration-150 hover:border-st-ink has-checked:border-st-accent has-checked:bg-st-soft has-checked:shadow-[inset_0_0_0_1px_var(--color-st-accent)] motion-reduce:transition-none"><input class="mt-px size-[18px] cursor-pointer accent-st-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="radio" name="delivery" value="collect" data-ship-option data-label="Collect from the studio" data-price="0.00"><span><strong class="block font-semibold">Collect from the studio</strong><span class="text-[.82rem] text-st-muted">Ready tomorrow · 4 Wharf Lane</span></span><b class="tabular-nums">Free</b></label>
          </fieldset>
          <div class="mt-[22px] flex justify-between gap-3"><button class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-white px-5 py-3 font-bold text-st-accent transition-colors duration-150 hover:bg-st-soft motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="ml-auto inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-st-accent px-5 py-3 font-bold text-white transition-colors duration-150 hover:bg-st-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="button" data-next hidden>Continue to payment</button></div>
        </section>
        <section class="[&:not([hidden])+&]:mt-7 [&:not([hidden])+&]:border-t [&:not([hidden])+&]:border-st-line [&:not([hidden])+&]:pt-6" data-step-panel aria-labelledby="st-h3">
          <h2 class="mb-1 text-[1.2rem] font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" id="st-h3" tabindex="-1">Payment</h2>
          <p class="mb-[18px] text-st-muted">Card details for this demo checkout.</p>
          <div class="mb-4 flex gap-3 rounded-xl border border-[#e7c98f] bg-[#fff7e6] px-4 py-3.5 text-[.85rem] text-[#5c3b06] [&_strong]:block [&>svg]:mt-px [&>svg]:flex-none"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8-8 9-4.6-1-8-4.5-8-9V6z"/><path d="M12 8v4M12 16h.01"/></svg><p><strong>Demo payment form</strong>Not connected to a payment provider. Nothing you type is sent or stored and no card is charged.</p></div>
          <div class="grid grid-cols-6 gap-3.5">
            <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-card">Card number</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-card" type="text" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]{13,23}" placeholder="1234 1234 1234 1234" required aria-describedby="st-card-err" data-missing="Enter a card number." data-bad="Enter the 13 to 19 digits on the front of the card."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-card-err"></p></div>
            <div class="col-span-3 min-[576px]:col-span-3"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-exp">Expiry date</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-exp" type="text" autocomplete="cc-exp" inputmode="numeric" pattern="(0[1-9]|1[0-2]) ?/ ?[0-9]{2}" placeholder="MM / YY" required aria-describedby="st-exp-err" data-missing="Enter the expiry date." data-bad="Use the format MM / YY."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-exp-err"></p></div>
            <div class="col-span-3 min-[576px]:col-span-3"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-cvc">Security code</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-cvc" type="text" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]{3,4}" required aria-describedby="st-cvc-hint st-cvc-err" data-missing="Enter the security code." data-bad="Enter the 3 or 4 digit code."><p class="mt-[5px] text-[.78rem] text-st-muted" id="st-cvc-hint">3 or 4 digits on the back.</p><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-cvc-err"></p></div>
            <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="st-ccname">Name on card</label><input class="block w-full rounded-[10px] border border-st-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7f727a] focus:border-st-accent focus:shadow-[0_0_0_3px_rgba(122,46,79,.25)] focus:outline-none aria-invalid:border-st-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="st-ccname" type="text" autocomplete="cc-name" required aria-describedby="st-ccname-err" data-missing="Enter the name on the card."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-st-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-st-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="st-ccname-err"></p></div>
          </div>
          <div class="mt-[22px] flex justify-between gap-3"><button class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-white px-5 py-3 font-bold text-st-accent transition-colors duration-150 hover:bg-st-soft motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="ml-auto inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-st-accent px-5 py-3 font-bold text-white transition-colors duration-150 hover:bg-st-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="button" data-next hidden>Continue to review</button></div>
        </section>
        <section class="[&:not([hidden])+&]:mt-7 [&:not([hidden])+&]:border-t [&:not([hidden])+&]:border-st-line [&:not([hidden])+&]:pt-6" data-step-panel aria-labelledby="st-h4">
          <h2 class="mb-1 text-[1.2rem] font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" id="st-h4" tabindex="-1">Review your order</h2>
          <p class="mb-[18px] text-st-muted">Check everything before you place the order.</p>
          <dl class="rounded-xl border border-st-line">
            <div class="grid grid-cols-[110px_minmax(0,1fr)] gap-3 px-4 py-3 [&+&]:border-t [&+&]:border-st-line max-[575px]:grid-cols-1 max-[575px]:gap-0.5"><dt class="text-[.84rem] text-st-muted">Email</dt><dd class="font-semibold wrap-anywhere"><span data-review="st-email">Not given</span></dd></div>
            <div class="grid grid-cols-[110px_minmax(0,1fr)] gap-3 px-4 py-3 [&+&]:border-t [&+&]:border-st-line max-[575px]:grid-cols-1 max-[575px]:gap-0.5"><dt class="text-[.84rem] text-st-muted">Ship to</dt><dd class="font-semibold wrap-anywhere"><span data-review="st-name">Not given</span>, <span data-review="st-address">Not given</span>, <span data-review="st-city">Not given</span> <span data-review="st-zip">Not given</span></dd></div>
            <div class="grid grid-cols-[110px_minmax(0,1fr)] gap-3 px-4 py-3 [&+&]:border-t [&+&]:border-st-line max-[575px]:grid-cols-1 max-[575px]:gap-0.5"><dt class="text-[.84rem] text-st-muted">Delivery</dt><dd class="font-semibold wrap-anywhere"><span data-ship-label>Insured standard</span></dd></div>
            <div class="grid grid-cols-[110px_minmax(0,1fr)] gap-3 px-4 py-3 [&+&]:border-t [&+&]:border-st-line max-[575px]:grid-cols-1 max-[575px]:gap-0.5"><dt class="text-[.84rem] text-st-muted">Payment</dt><dd class="font-semibold wrap-anywhere"><span data-review-card="st-card">Not given</span> (demo)</dd></div>
          </dl>
          <p class="text-[.85rem] font-semibold text-st-ok [&:not(:empty)]:mt-3.5 [&:not(:empty)]:rounded-[10px] [&:not(:empty)]:bg-[#f0fdf4] [&:not(:empty)]:px-3.5 [&:not(:empty)]:py-3" role="status" data-status></p>
          <div class="mt-[22px] flex justify-between gap-3"><button class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-white px-5 py-3 font-bold text-st-accent transition-colors duration-150 hover:bg-st-soft motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="button" data-prev hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8H3M7 4L3 8l4 4"/></svg>Back</button><button class="ml-auto inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-st-accent bg-st-accent px-5 py-3 font-bold text-white transition-colors duration-150 hover:bg-st-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent" type="submit"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>Place demo order</button></div>
        </section>
    </div>
    <aside class="sticky top-5 rounded-[18px] bg-white shadow-[0_1px_2px_rgba(34,22,28,.05),0_16px_40px_-24px_rgba(34,22,28,.32)] p-[22px] max-[900px]:static" aria-labelledby="st-sum-title">
      <h2 class="mb-1.5 text-[1.05rem] font-bold" id="st-sum-title">Order summary</h2>
      <ul class="border-b border-st-line pb-1.5">
        <li class="grid grid-cols-[52px_minmax(0,1fr)_auto] items-center gap-3 py-2"><img class="size-[52px] rounded-[10px] bg-st-soft object-cover" src="https://images.unsplash.com/photo-1603561591411-07134e71a2a9?w=104&h=104&fit=crop&auto=format&q=70" alt="" width="52" height="52"><div><p class="text-[.88rem] leading-[1.3] font-semibold">Pink tourmaline ring</p><p class="text-[.78rem] text-st-muted">Rose gold · Size 6 · Qty 1</p></div><p class="font-semibold tabular-nums">$185.00</p></li>
        <li class="grid grid-cols-[52px_minmax(0,1fr)_auto] items-center gap-3 py-2"><img class="size-[52px] rounded-[10px] bg-st-soft object-cover" src="https://images.unsplash.com/photo-1535632066927-ab7c9ab60908?w=104&h=104&fit=crop&auto=format&q=70" alt="" width="52" height="52"><div><p class="text-[.88rem] leading-[1.3] font-semibold">Blue topaz drop earrings</p><p class="text-[.78rem] text-st-muted">Sterling silver · Qty 1</p></div><p class="font-semibold tabular-nums">$96.00</p></li>
      </ul>
      <dl class="grid gap-2 border-b border-st-line py-3.5">
        <div class="flex justify-between gap-3"><dt class="text-st-muted">Subtotal</dt><dd class="font-semibold tabular-nums" data-subtotal>$281.00</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-st-muted">Delivery</dt><dd class="font-semibold tabular-nums" data-shipping>$8.00</dd></div>
      </dl>
      <dl class="flex items-baseline justify-between pt-3.5 font-bold"><dt>Total</dt><dd class="text-[1.3rem] font-extrabold tabular-nums" data-total>$289.00</dd></dl>
    </aside>
  </div>
</form>
</div>
<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] items-start gap-[34px] text-[.93rem] leading-[1.45] text-st-ink">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#625560] uppercase">Compact progress bar</span>
<nav class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(34,22,28,.05),0_16px_40px_-24px_rgba(34,22,28,.32)] p-7 max-[575px]:px-[18px] max-[575px]:py-[22px]" aria-label="Checkout progress, compact">
  <p class="mb-2.5 flex justify-between gap-3 text-[.88rem]"><strong>Step 2 of 4: Delivery</strong><span class="text-st-muted">Next: Payment</span></p>
  <ol class="grid grid-cols-4 gap-1.5">
    <li class="h-2 rounded-full bg-st-line data-[state=done]:bg-st-accent data-[state=current]:bg-[linear-gradient(90deg,var(--color-st-accent)_50%,#d9c3cd_50%)]" data-state="done"><span class="sr-only">Details, completed</span></li>
    <li class="h-2 rounded-full bg-st-line data-[state=done]:bg-st-accent data-[state=current]:bg-[linear-gradient(90deg,var(--color-st-accent)_50%,#d9c3cd_50%)]" data-state="current" aria-current="step"><span class="sr-only">Delivery</span></li>
    <li class="h-2 rounded-full bg-st-line data-[state=done]:bg-st-accent data-[state=current]:bg-[linear-gradient(90deg,var(--color-st-accent)_50%,#d9c3cd_50%)]" data-state="next"><span class="sr-only">Payment</span></li>
    <li class="h-2 rounded-full bg-st-line data-[state=done]:bg-st-accent data-[state=current]:bg-[linear-gradient(90deg,var(--color-st-accent)_50%,#d9c3cd_50%)]" data-state="next"><span class="sr-only">Review</span></li>
  </ol>
</nav>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#625560] uppercase">Vertical steps</span>
<nav class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(34,22,28,.05),0_16px_40px_-24px_rgba(34,22,28,.32)] p-7 max-[575px]:px-[18px] max-[575px]:py-[22px]" aria-label="Checkout progress, vertical">
  <ol>
    <li class="group relative grid grid-cols-[32px_minmax(0,1fr)] gap-3.5 pb-5 text-st-muted last:pb-0 before:absolute before:top-[34px] before:bottom-0.5 before:left-[15px] before:w-0.5 before:bg-st-line last:before:hidden data-[state=done]:before:bg-st-accent" data-state="done"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>1</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong class="mt-[5px] block text-[.93rem] text-st-ink">Details<span class="sr-only">, completed</span></strong><small class="text-[.8rem]">[email protected]</small></div></li>
    <li class="group relative grid grid-cols-[32px_minmax(0,1fr)] gap-3.5 pb-5 text-st-muted last:pb-0 before:absolute before:top-[34px] before:bottom-0.5 before:left-[15px] before:w-0.5 before:bg-st-line last:before:hidden data-[state=done]:before:bg-st-accent" data-state="done"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>2</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong class="mt-[5px] block text-[.93rem] text-st-ink">Delivery<span class="sr-only">, completed</span></strong><small class="text-[.8rem]">Insured express, next business day</small></div></li>
    <li class="group relative grid grid-cols-[32px_minmax(0,1fr)] gap-3.5 pb-5 text-st-muted last:pb-0 before:absolute before:top-[34px] before:bottom-0.5 before:left-[15px] before:w-0.5 before:bg-st-line last:before:hidden data-[state=done]:before:bg-st-accent" data-state="current" aria-current="step"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>3</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong class="mt-[5px] block text-[.93rem] text-st-ink">Payment</strong><small class="text-[.8rem]">Card details</small></div></li>
    <li class="group relative grid grid-cols-[32px_minmax(0,1fr)] gap-3.5 pb-5 text-st-muted last:pb-0 before:absolute before:top-[34px] before:bottom-0.5 before:left-[15px] before:w-0.5 before:bg-st-line last:before:hidden data-[state=done]:before:bg-st-accent" data-state="next"><span class="relative z-[1] grid size-8 place-items-center rounded-full border-2 border-st-field bg-white text-[.82rem] [&>svg]:hidden group-data-[state=current]:border-st-accent group-data-[state=current]:text-st-accent group-data-[state=current]:shadow-[0_0_0_4px_rgba(122,46,79,.25)] group-data-[state=done]:border-st-accent group-data-[state=done]:bg-st-accent group-data-[state=done]:text-white group-data-[state=done]:[&>span]:hidden group-data-[state=done]:[&>svg]:block" aria-hidden="true"><span>4</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><div><strong class="mt-[5px] block text-[.93rem] text-st-ink">Review</strong><small class="text-[.8rem]">Check and place the order</small></div></li>
  </ol>
</nav>
</div>
</div>
CSS
@theme {
  --color-st-accent: #7a2e4f;
  --color-st-accent-dark: #5f2240;
  --color-st-ink: #22161c;
  --color-st-muted: #625560;
  --color-st-line: #ebe3e7;
  --color-st-field: #8a7d85;
  --color-st-soft: #faf5f7;
  --color-st-error: #b42318;
  --color-st-ok: #166534;
}
JavaScript
// Checkout form (demo): nothing is sent and no card is charged. On submit every field is checked with the
// browser's own validity rules; a failing field gets aria-invalid and the message from its data-missing or
// data-bad attribute in the element with id "<field id>-err" (already in its aria-describedby), and focus
// moves to the first problem. [data-ship-option] radios update shipping and the total in the summary.
document.querySelectorAll('form[data-checkout]').forEach(function (form) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var fmt = function (c) { return money.format(c / 100); };
  var rate = parseFloat(form.dataset.tax || 0), status = form.querySelector('[data-status]');
  function put(sel, text) { form.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = cents(form.dataset.subtotal), opt = form.querySelector('[data-ship-option]:checked');
    var ship = opt ? cents(opt.dataset.price) : 0, tax = Math.round(sub * rate / 100), total = sub + ship + tax;
    put('[data-subtotal]', fmt(sub)); put('[data-tax]', fmt(tax)); put('[data-total]', fmt(total));
    put('[data-shipping]', ship ? fmt(ship) : 'Free');
    if (opt) put('[data-ship-label]', opt.dataset.label);
    return total;
  }
  function check(field) {
    var v = field.validity, msg = v.valid ? '' : v.valueMissing ? field.dataset.missing : (field.dataset.bad || field.validationMessage);
    var out = document.getElementById(field.id + '-err');
    field.classList.toggle('is-invalid', !!msg);
    if (msg) field.setAttribute('aria-invalid', 'true'); else field.removeAttribute('aria-invalid');
    if (out) out.textContent = msg || '';
    return !msg;
  }
  form.noValidate = true;
  form.addEventListener('change', function (e) {
    if (!e.target.matches('[data-ship-option]')) return;
    var total = render();
    if (status) status.textContent = e.target.dataset.label + ' selected. Total ' + fmt(total) + '.';
  });
  form.addEventListener('input', function (e) { if (e.target.hasAttribute('aria-invalid')) check(e.target); });
  form.addEventListener('submit', function (e) {
    e.preventDefault(); // demo only: nothing is sent anywhere
    var bad = Array.prototype.filter.call(form.elements, function (f) { return f.willValidate && f.id && !check(f); });
    if (bad.length) { if (status) status.textContent = ''; bad[0].focus(); return; }
    if (status) status.textContent = 'Demo order placed. Nothing was charged: this form is not connected to a payment provider.';
  });
  render();
});

// Multi-step checkout: one form split into [data-step-panel] sections. Continue checks only the fields in the
// current step, then shows the next one, moves aria-current="step" in the progress list and puts focus on the
// new step's heading. Without the script every step stays visible and the form works as one long page.
document.querySelectorAll('form[data-steps]').forEach(function (form) {
  var panels = [].slice.call(form.querySelectorAll('[data-step-panel]'));
  var marks = [].slice.call(form.querySelectorAll('[data-step-mark]'));
  var at = 0;
  function check(field) {
    var v = field.validity, msg = v.valid ? '' : v.valueMissing ? field.dataset.missing : (field.dataset.bad || field.validationMessage);
    var out = document.getElementById(field.id + '-err');
    field.classList.toggle('is-invalid', !!msg);
    if (msg) field.setAttribute('aria-invalid', 'true'); else field.removeAttribute('aria-invalid');
    if (out) out.textContent = msg || '';
    return !msg;
  }
  function show(n, focus) {
    at = n;
    panels.forEach(function (p, i) { p.hidden = i !== n; });
    marks.forEach(function (m, i) {
      if (i === n) m.setAttribute('aria-current', 'step'); else m.removeAttribute('aria-current');
      m.dataset.state = i < n ? 'done' : i === n ? 'current' : 'next';
      m.querySelector('[data-step-sr]').textContent = i < n ? ', completed' : '';
    });
    form.querySelectorAll('[data-review]').forEach(function (el) {
      var f = document.getElementById(el.dataset.review);
      el.textContent = f && f.value.trim() ? f.value.trim() : 'Not given';
    });
    form.querySelectorAll('[data-review-card]').forEach(function (el) {
      var digits = document.getElementById(el.dataset.reviewCard).value.replace(/\D/g, '');
      el.textContent = digits ? 'Card ending ' + digits.slice(-4) : 'Not given';
    });
    if (focus) panels[n].querySelector('[tabindex="-1"]').focus();
  }
  form.querySelectorAll('[data-next],[data-prev]').forEach(function (b) { b.hidden = false; });
  form.addEventListener('click', function (e) {
    if (e.target.closest('[data-prev]')) return show(at - 1, true);
    if (!e.target.closest('[data-next]')) return;
    var bad = [].filter.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return f.willValidate && f.id && !check(f); });
    if (bad.length) bad[0].focus(); else show(at + 1, true);
  });
  show(0, false);
});

About this set

A multi-step checkout for a jewellery studio and two other ways to show checkout progress. Numbered stepper checkout splits one form into Details, Delivery, Payment and Review. A numbered step indicator marks the current step with aria-current=”step”, fills in finished steps with a tick and visually hidden completed text, and joins them with a line. Continue checks only the fields in the current step, shows an inline message for each problem and moves focus to the first, and once the step passes it shows the next panel and moves focus to its heading. The delivery step updates the order summary beside the form, the payment step is a clearly labelled demo with no name attributes on the card fields, and the review step reads back the email, address, delivery choice and the last four card digits before Place demo order. Without JavaScript every step stays visible and the form works as one long page. Compact progress bar shows Step 2 of 4 with four segments, and Vertical steps lists each step with a short detail underneath, both marked up as navigation lists with aria-current.

What’s included

  • Four-step checkout in a single form with a sticky order summary
  • Step indicator with aria-current="step" and completed states
  • Per-step validation with inline errors and focus management
  • Review step that reads back the entered details
  • Compact progress bar and vertical step list variants

Accessibility

The progress list is a nav with an ordered list; the current step has aria-current="step" and finished steps add visually hidden completed text. Moving between steps puts focus on the new step's heading, errors are linked with aria-describedby and marked with aria-invalid, and every field has a visible label and an autocomplete token. The demo order message is announced in a polite status region, and transitions stop under prefers-reduced-motion.

Customise it

Change --st-accent, --st-line and --st-field on .st; the step names live in the progress list and the panel headings. Delivery prices are data-price on each radio. In Bootstrap the fields use .row and .col-*; in Tailwind edit the st-* colours in @theme.