Open demo

CSS code

HTML
<div><span class="lbl">Single-page checkout</span>
<form class="co" action="#" method="post" data-checkout data-subtotal="80.00" data-tax="7" aria-labelledby="co-title">
  <div class="co-layout">
    <div class="co-main">
      <p class="co-brand"><span aria-hidden="true">R</span>Ridgeline Coffee</p>
      <h2 class="sr-only" id="co-title">Checkout</h2>
      <p class="co-note">All fields are required unless marked optional.</p>
      <fieldset class="co-set"><legend class="co-legend"><i aria-hidden="true">1</i>Contact</legend>
        <div class="co-grid">
          <div class="co-s6 co-m6"><label class="co-label" for="co-email">Email</label><input class="co-input" id="co-email" name="email" type="email" autocomplete="email" required aria-describedby="co-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected]."><p class="co-err" id="co-email-err"></p></div>
        </div>
        <label class="co-check"><input type="checkbox" name="news"><span>Email me when new roasts land (optional)</span></label>
      </fieldset>
      <fieldset class="co-set"><legend class="co-legend"><i aria-hidden="true">2</i>Shipping address</legend>
        <div class="co-grid">
          <div class="co-s6 co-m6"><label class="co-label" for="co-country">Country or region</label><select class="co-input" id="co-country" name="country" autocomplete="country"><option value="US" selected>United States</option><option value="CA">Canada</option><option value="GB">United Kingdom</option><option value="AU">Australia</option></select></div>
          <div class="co-s3 co-m6"><label class="co-label" for="co-first">First name</label><input class="co-input" id="co-first" name="first-name" type="text" autocomplete="shipping given-name" required aria-describedby="co-first-err" data-missing="Enter your first name."><p class="co-err" id="co-first-err"></p></div>
          <div class="co-s3 co-m6"><label class="co-label" for="co-last">Last name</label><input class="co-input" id="co-last" name="last-name" type="text" autocomplete="shipping family-name" required aria-describedby="co-last-err" data-missing="Enter your last name."><p class="co-err" id="co-last-err"></p></div>
          <div class="co-s6 co-m6"><label class="co-label" for="co-address">Street address</label><input class="co-input" id="co-address" name="address" type="text" autocomplete="shipping address-line1" required aria-describedby="co-address-err" data-missing="Enter your street address."><p class="co-err" id="co-address-err"></p></div>
          <div class="co-s6 co-m6"><label class="co-label" for="co-address2">Apartment, suite, etc. (optional)</label><input class="co-input" id="co-address2" name="address2" type="text" autocomplete="shipping address-line2"></div>
          <div class="co-s2 co-m6"><label class="co-label" for="co-city">City</label><input class="co-input" id="co-city" name="city" type="text" autocomplete="shipping address-level2" required aria-describedby="co-city-err" data-missing="Enter your city."><p class="co-err" id="co-city-err"></p></div>
          <div class="co-s2 co-m3"><label class="co-label" for="co-state">State</label><input class="co-input" id="co-state" name="state" type="text" autocomplete="shipping address-level1" required aria-describedby="co-state-err" data-missing="Enter your state."><p class="co-err" id="co-state-err"></p></div>
          <div class="co-s2 co-m3"><label class="co-label" for="co-zip">ZIP code</label><input class="co-input" id="co-zip" name="zip" type="text" autocomplete="shipping postal-code" inputmode="numeric" pattern="[0-9]{5}(-[0-9]{4})?" required aria-describedby="co-zip-err" data-missing="Enter your ZIP code." data-bad="Enter a 5-digit ZIP code, like 97214."><p class="co-err" id="co-zip-err"></p></div>
          <div class="co-s6 co-m6"><label class="co-label" for="co-phone">Phone (optional)</label><input class="co-input" id="co-phone" name="phone" type="tel" autocomplete="shipping tel" aria-describedby="co-phone-hint co-phone-err"><p class="co-hint" id="co-phone-hint">Only used to text you about this delivery.</p></div>
        </div>
      </fieldset>
      <fieldset class="co-set"><legend class="co-legend"><i aria-hidden="true">3</i>Delivery</legend>
        <div class="co-options">
          <label class="co-radio"><input type="radio" name="delivery" value="std" data-ship-option data-label="Standard" data-price="5.00" checked><span><strong>Standard</strong><span>3 to 5 business days</span></span><b>$5.00</b></label>
          <label class="co-radio"><input type="radio" name="delivery" value="exp" data-ship-option data-label="Express" data-price="14.00"><span><strong>Express</strong><span>Next business day</span></span><b>$14.00</b></label>
          <label class="co-radio"><input type="radio" name="delivery" value="pick" data-ship-option data-label="Pick up at the roastery" data-price="0.00"><span><strong>Pick up at the roastery</strong><span>Ready in 2 hours · 18 Alder Street</span></span><b>Free</b></label>
        </div>
      </fieldset>
      <fieldset class="co-set"><legend class="co-legend"><i aria-hidden="true">4</i>Payment</legend>
        <div class="co-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>These fields are not connected to a payment provider. Nothing you type is sent or stored and no card is charged. In production, use your provider's secure card fields.</p></div>
        <div class="co-grid">
          <div class="co-s6 co-m6"><label class="co-label" for="co-card">Card number</label><input class="co-input" id="co-card" type="text" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]{13,23}" placeholder="1234 1234 1234 1234" required aria-describedby="co-card-err" data-missing="Enter a card number." data-bad="Enter the 13 to 19 digits on the front of the card."><p class="co-err" id="co-card-err"></p></div>
          <div class="co-s3 co-m3"><label class="co-label" for="co-exp">Expiry date</label><input class="co-input" id="co-exp" type="text" autocomplete="cc-exp" inputmode="numeric" pattern="(0[1-9]|1[0-2]) ?/ ?[0-9]{2}" placeholder="MM / YY" required aria-describedby="co-exp-err" data-missing="Enter the expiry date." data-bad="Use the format MM / YY."><p class="co-err" id="co-exp-err"></p></div>
          <div class="co-s3 co-m3"><label class="co-label" for="co-cvc">Security code</label><input class="co-input" id="co-cvc" type="text" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]{3,4}" required aria-describedby="co-cvc-hint co-cvc-err" data-missing="Enter the security code." data-bad="Enter the 3 or 4 digit code."><p class="co-hint" id="co-cvc-hint">3 or 4 digits on the back.</p><p class="co-err" id="co-cvc-err"></p></div>
          <div class="co-s6 co-m6"><label class="co-label" for="co-ccname">Name on card</label><input class="co-input" id="co-ccname" type="text" autocomplete="cc-name" required aria-describedby="co-ccname-err" data-missing="Enter the name on the card."><p class="co-err" id="co-ccname-err"></p></div>
        </div>
      </fieldset>
    </div>
    <aside class="co-sum" aria-labelledby="co-sum-title">
      <h2 class="co-sum-title" id="co-sum-title">Order summary</h2>
      <ul class="co-items">
        <li class="co-item"><div class="co-thumb"><img src="https://images.unsplash.com/photo-1447933601403-0c6688de566e?w=112&h=112&fit=crop&auto=format&q=70" alt="" width="56" height="56"><span class="co-qty" aria-hidden="true">2</span></div>
          <div><p class="co-name">Ethiopia Guji, whole bean</p><p class="co-meta">340 g · Light roast<span class="sr-only">, quantity 2</span></p></div><p class="co-price">$38.00</p></li>
        <li class="co-item"><div class="co-thumb"><img src="https://images.unsplash.com/photo-1517668808822-9ebb02f2a0e6?w=112&h=112&fit=crop&auto=format&q=70" alt="" width="56" height="56"><span class="co-qty" aria-hidden="true">1</span></div>
          <div><p class="co-name">House espresso, whole bean</p><p class="co-meta">1 kg · Medium-dark roast<span class="sr-only">, quantity 1</span></p></div><p class="co-price">$42.00</p></li>
      </ul>
      <dl class="co-rows">
        <div class="co-row"><dt>Subtotal</dt><dd data-subtotal>$80.00</dd></div>
        <div class="co-row"><dt>Shipping (<span data-ship-label>Standard</span>)</dt><dd data-shipping>$5.00</dd></div>
        <div class="co-row"><dt>Estimated tax (7%)</dt><dd data-tax>$5.60</dd></div>
      </dl>
      <dl class="co-total"><dt>Total</dt><dd data-total>$90.60</dd></dl>
      <button class="co-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>
      <p class="co-fine">Demo checkout. No payment is taken.</p>
      <p class="co-status" role="status" data-status></p>
    </aside>
  </div>
</form>
</div>
<div><span class="lbl">Order confirmation</span>
<section class="co co-done" aria-labelledby="co-done-title">
  <div class="co-done-mark"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></div>
  <h2 id="co-done-title">Thanks, Maya. Your order is in.</h2>
  <p class="co-done-lead">Order RC-20417. We roast on Tuesdays and Fridays, so your beans will be at most two days old when they ship.</p>
  <dl class="co-facts">
    <div><dt>Delivery</dt><dd>Standard, 3 to 5 business days</dd></div>
    <div><dt>Ship to</dt><dd>Maya Lindqvist, 2210 SE Clinton St, Portland</dd></div>
    <div><dt>Items</dt><dd>3 items</dd></div>
    <div><dt>Total</dt><dd>$90.60</dd></div>
  </dl>
  <div class="co-done-actions"><a href="#">Track your order</a><a href="#">Continue shopping</a></div>
</section>
</div>
CSS
/* Single-page checkout. One <form> holds the fields and the summary; every field has a visible label and a
   checkout autocomplete token. The card fields are a front-end mock: they have no name attribute, so the form
   would never submit them, and the script stops the demo submit anyway. */
.co{--co-accent:#7a3e0c;--co-accent-dark:#62310a;--co-ink:#211812;--co-muted:#5f544b;--co-line:#e7e0d8;--co-field:#8b8178;--co-soft:#faf6f1;
  --co-ring:rgba(122,62,12,.25);--co-error:#b42318;--co-ok:#166534;color:var(--co-ink);font-size:.93rem;line-height:1.45}
.co [hidden]{display:none!important}
.co .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}
.co :where(button,input,select){font:inherit;color:inherit}
.co :is(a,button):focus-visible{outline:2px solid var(--co-accent);outline-offset:2px}
.co-layout{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:28px;align-items:start}
.co-main{background:#fff;border-radius:18px;padding:28px;box-shadow:0 1px 2px rgba(33,24,18,.05),0 16px 40px -24px rgba(33,24,18,.3)}
.co-brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;font-size:.88rem;margin-bottom:6px}
.co-brand span{width:30px;height:30px;border-radius:50%;background:var(--co-accent);color:#fff;display:grid;place-items:center;font-size:.8rem}
.co-note{color:var(--co-muted);font-size:.84rem;margin-bottom:6px}
.co-set{border:0;padding:22px 0 4px;margin:0;min-width:0}
.co-set+.co-set{border-top:1px solid var(--co-line);margin-top:18px}
.co-legend{float:left;width:100%;font-size:1.1rem;font-weight:700;padding:0;margin-bottom:14px;display:flex;align-items:center;gap:10px}
.co-legend+*{clear:left}
.co-legend i{font-style:normal;width:26px;height:26px;border-radius:50%;background:var(--co-soft);border:1px solid var(--co-line);display:grid;place-items:center;font-size:.78rem;font-weight:700}
.co-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px}
.co-s6{grid-column:span 6}.co-s3{grid-column:span 3}.co-s2{grid-column:span 2}
.co-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.co-input{display:block;width:100%;padding:11px 13px;border:1px solid var(--co-field);border-radius:10px;background:#fff;transition:border-color .15s,box-shadow .15s}
.co-input::placeholder{color:#7d7268}
select.co-input{appearance:none;padding-right:38px;cursor:pointer;
  background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%235f544b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 12px center/16px no-repeat}
.co-input:focus{outline:none;border-color:var(--co-accent);box-shadow:0 0 0 3px var(--co-ring)}
.co-input[aria-invalid="true"]{border-color:var(--co-error)}
.co-input[aria-invalid="true"]:focus{box-shadow:0 0 0 3px rgba(180,35,24,.2)}
.co-hint{font-size:.78rem;color:var(--co-muted);margin-top:5px}
.co-err{display:flex;gap:6px;color:var(--co-error);font-size:.8rem;font-weight:600;margin-top:6px}
.co-err::before{content:"!";flex:none;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--co-error);color:#fff;font-size:.7rem;font-weight:800;display:grid;place-items:center}
.co-err:empty{display:none}
.co-check{display:flex;gap:10px;align-items:flex-start;margin-top:12px;font-size:.88rem;cursor:pointer}
.co-check input,.co-radio input{flex:none;width:18px;height:18px;margin:1px 0 0;accent-color:var(--co-accent);cursor:pointer}
.co-check input:focus-visible,.co-radio input:focus-visible{outline:2px solid var(--co-accent);outline-offset:2px}
/* delivery radio cards */
.co-options{display:grid;gap:10px}
.co-radio{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:start;padding:14px 16px;border:1px solid var(--co-field);border-radius:12px;cursor:pointer;transition:border-color .15s,background-color .15s}
.co-radio:hover{border-color:var(--co-ink)}
.co-radio:has(:checked){border-color:var(--co-accent);background:var(--co-soft);box-shadow:inset 0 0 0 1px var(--co-accent)}
.co-radio strong{display:block;font-weight:600}
.co-radio span span{font-size:.82rem;color:var(--co-muted)}
.co-radio b{font-variant-numeric:tabular-nums}
.co-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}
.co-demo svg{flex:none;margin-top:1px}
.co-demo strong{display:block}
/* summary */
.co-sum{position:sticky;top:20px;background:#fff;border-radius:18px;padding:24px;box-shadow:0 1px 2px rgba(33,24,18,.05),0 16px 40px -24px rgba(33,24,18,.3)}
.co-sum-title{font-size:1.1rem;font-weight:700;margin-bottom:8px}
.co-items{list-style:none;border-bottom:1px solid var(--co-line);padding-bottom:8px}
.co-item{display:grid;grid-template-columns:56px minmax(0,1fr) auto;gap:12px;align-items:center;padding:8px 0}
.co-thumb{position:relative}
.co-thumb img{width:56px;height:56px;border-radius:10px;object-fit:cover;background:var(--co-soft)}
.co-qty{position:absolute;top:-6px;right:-6px;min-width:21px;height:21px;padding:0 5px;border-radius:999px;background:var(--co-muted);color:#fff;font-size:.7rem;font-weight:700;display:grid;place-items:center}
.co-name{font-weight:600;line-height:1.3;font-size:.9rem}
.co-meta{font-size:.8rem;color:var(--co-muted)}
.co-price{font-weight:600;font-variant-numeric:tabular-nums}
.co-rows{display:grid;gap:8px;padding:14px 0;border-bottom:1px solid var(--co-line)}
.co-row{display:flex;justify-content:space-between;gap:12px}
.co-row dt{color:var(--co-muted)}
.co-row dd{font-weight:600;font-variant-numeric:tabular-nums}
.co-total{display:flex;justify-content:space-between;align-items:baseline;padding:14px 0 4px;font-size:1.05rem;font-weight:700}
.co-total dd{font-size:1.35rem;font-weight:800;font-variant-numeric:tabular-nums}
.co-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:16px;padding:14px 18px;border:0;border-radius:12px;background:var(--co-accent);color:#fff;
  font-weight:700;cursor:pointer;transition:background-color .15s}
.co-btn:hover{background:var(--co-accent-dark)}
.co-fine{font-size:.78rem;color:var(--co-muted);text-align:center;margin-top:10px}
.co-status:not(:empty){margin-top:14px;padding:12px 14px;border-radius:10px;background:#f0fdf4;color:var(--co-ok);font-weight:600;font-size:.85rem}
/* confirmation */
.co-done{max-width:560px;background:#fff;border-radius:18px;padding:30px;box-shadow:0 1px 2px rgba(33,24,18,.05),0 16px 40px -24px rgba(33,24,18,.3)}
.co-done-mark{width:56px;height:56px;border-radius:50%;background:#e8f5ec;color:var(--co-ok);display:grid;place-items:center;margin-bottom:14px}
.co-done h2{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:1.7rem;line-height:1.15}
.co-done-lead{color:var(--co-muted);margin:6px 0 18px}
.co-facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;padding:16px 0;border-block:1px solid var(--co-line);margin-bottom:18px}
.co-facts dt{font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;color:var(--co-muted);font-weight:600}
.co-facts dd{font-weight:600;margin-top:2px}
.co-done-actions{display:flex;flex-wrap:wrap;gap:10px}
.co-done-actions a{flex:1;min-width:160px;display:flex;justify-content:center;padding:12px 16px;border-radius:12px;font-weight:700;text-decoration:none;border:1px solid var(--co-accent)}
.co-done-actions a:first-child{background:var(--co-accent);color:#fff}
.co-done-actions a:last-child{color:var(--co-accent)}
@media (max-width:900px){.co-layout{grid-template-columns:1fr}.co-sum{position:static}}
@media (max-width:575px){.co-main{padding:22px 18px}.co-m6{grid-column:span 6}.co-m3{grid-column:span 3}.co-facts{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.co-input,.co-radio,.co-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();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Single-page checkout</span>
<form class="co" action="#" method="post" data-checkout data-subtotal="80.00" data-tax="7" aria-labelledby="co-title">
  <div class="co-layout">
    <div class="card co-main"><div class="card-body">
      <p class="co-brand"><span aria-hidden="true">R</span>Ridgeline Coffee</p>
      <h2 class="visually-hidden" id="co-title">Checkout</h2>
      <p class="co-note">All fields are required unless marked optional.</p>
      <fieldset class="co-set"><legend class="co-legend mb-0"><i aria-hidden="true">1</i>Contact</legend>
        <div class="row">
          <div class="col-12 col-sm-12"><label class="form-label" for="co-email">Email</label><input class="form-control" id="co-email" name="email" type="email" autocomplete="email" required aria-describedby="co-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected]."><p class="invalid-feedback" id="co-email-err"></p></div>
        </div>
        <div class="form-check"><input class="form-check-input" id="co-news" type="checkbox" name="news"><label class="form-check-label" for="co-news">Email me when new roasts land (optional)</label></div>
      </fieldset>
      <fieldset class="co-set"><legend class="co-legend mb-0"><i aria-hidden="true">2</i>Shipping address</legend>
        <div class="row">
          <div class="col-12 col-sm-12"><label class="form-label" for="co-country">Country or region</label><select class="form-select" id="co-country" name="country" autocomplete="country"><option value="US" selected>United States</option><option value="CA">Canada</option><option value="GB">United Kingdom</option><option value="AU">Australia</option></select></div>
          <div class="col-12 col-sm-6"><label class="form-label" for="co-first">First name</label><input class="form-control" id="co-first" name="first-name" type="text" autocomplete="shipping given-name" required aria-describedby="co-first-err" data-missing="Enter your first name."><p class="invalid-feedback" id="co-first-err"></p></div>
          <div class="col-12 col-sm-6"><label class="form-label" for="co-last">Last name</label><input class="form-control" id="co-last" name="last-name" type="text" autocomplete="shipping family-name" required aria-describedby="co-last-err" data-missing="Enter your last name."><p class="invalid-feedback" id="co-last-err"></p></div>
          <div class="col-12 col-sm-12"><label class="form-label" for="co-address">Street address</label><input class="form-control" id="co-address" name="address" type="text" autocomplete="shipping address-line1" required aria-describedby="co-address-err" data-missing="Enter your street address."><p class="invalid-feedback" id="co-address-err"></p></div>
          <div class="col-12 col-sm-12"><label class="form-label" for="co-address2">Apartment, suite, etc. (optional)</label><input class="form-control" id="co-address2" name="address2" type="text" autocomplete="shipping address-line2"></div>
          <div class="col-12 col-sm-4"><label class="form-label" for="co-city">City</label><input class="form-control" id="co-city" name="city" type="text" autocomplete="shipping address-level2" required aria-describedby="co-city-err" data-missing="Enter your city."><p class="invalid-feedback" id="co-city-err"></p></div>
          <div class="col-6 col-sm-4"><label class="form-label" for="co-state">State</label><input class="form-control" id="co-state" name="state" type="text" autocomplete="shipping address-level1" required aria-describedby="co-state-err" data-missing="Enter your state."><p class="invalid-feedback" id="co-state-err"></p></div>
          <div class="col-6 col-sm-4"><label class="form-label" for="co-zip">ZIP code</label><input class="form-control" id="co-zip" name="zip" type="text" autocomplete="shipping postal-code" inputmode="numeric" pattern="[0-9]{5}(-[0-9]{4})?" required aria-describedby="co-zip-err" data-missing="Enter your ZIP code." data-bad="Enter a 5-digit ZIP code, like 97214."><p class="invalid-feedback" id="co-zip-err"></p></div>
          <div class="col-12 col-sm-12"><label class="form-label" for="co-phone">Phone (optional)</label><input class="form-control" id="co-phone" name="phone" type="tel" autocomplete="shipping tel" aria-describedby="co-phone-hint co-phone-err"><p class="form-text" id="co-phone-hint">Only used to text you about this delivery.</p></div>
        </div>
      </fieldset>
      <fieldset class="co-set"><legend class="co-legend"><i aria-hidden="true">3</i>Delivery</legend>
        <div class="d-grid gap-2">
          <label class="form-check co-radio"><input class="form-check-input" type="radio" name="delivery" value="std" data-ship-option data-label="Standard" data-price="5.00" checked><span><strong>Standard</strong><span>3 to 5 business days</span></span><b>$5.00</b></label>
          <label class="form-check co-radio"><input class="form-check-input" type="radio" name="delivery" value="exp" data-ship-option data-label="Express" data-price="14.00"><span><strong>Express</strong><span>Next business day</span></span><b>$14.00</b></label>
          <label class="form-check co-radio"><input class="form-check-input" type="radio" name="delivery" value="pick" data-ship-option data-label="Pick up at the roastery" data-price="0.00"><span><strong>Pick up at the roastery</strong><span>Ready in 2 hours · 18 Alder Street</span></span><b>Free</b></label>
        </div>
      </fieldset>
      <fieldset class="co-set"><legend class="co-legend"><i aria-hidden="true">4</i>Payment</legend>
        <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>These fields are not connected to a payment provider. Nothing you type is sent or stored and no card is charged. In production, use your provider's secure card fields.</p></div>
        <div class="row">
          <div class="col-12 col-sm-12"><label class="form-label" for="co-card">Card number</label><input class="form-control" id="co-card" type="text" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]{13,23}" placeholder="1234 1234 1234 1234" required aria-describedby="co-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="co-card-err"></p></div>
          <div class="col-6 col-sm-6"><label class="form-label" for="co-exp">Expiry date</label><input class="form-control" id="co-exp" type="text" autocomplete="cc-exp" inputmode="numeric" pattern="(0[1-9]|1[0-2]) ?/ ?[0-9]{2}" placeholder="MM / YY" required aria-describedby="co-exp-err" data-missing="Enter the expiry date." data-bad="Use the format MM / YY."><p class="invalid-feedback" id="co-exp-err"></p></div>
          <div class="col-6 col-sm-6"><label class="form-label" for="co-cvc">Security code</label><input class="form-control" id="co-cvc" type="text" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]{3,4}" required aria-describedby="co-cvc-hint co-cvc-err" data-missing="Enter the security code." data-bad="Enter the 3 or 4 digit code."><p class="form-text" id="co-cvc-hint">3 or 4 digits on the back.</p><p class="invalid-feedback" id="co-cvc-err"></p></div>
          <div class="col-12 col-sm-12"><label class="form-label" for="co-ccname">Name on card</label><input class="form-control" id="co-ccname" type="text" autocomplete="cc-name" required aria-describedby="co-ccname-err" data-missing="Enter the name on the card."><p class="invalid-feedback" id="co-ccname-err"></p></div>
        </div>
      </fieldset>
    </div></div>
    <aside class="card co-sum" aria-labelledby="co-sum-title"><div class="card-body">
      <h2 class="co-sum-title" id="co-sum-title">Order summary</h2>
      <ul class="list-group">
        <li class="list-group-item co-item"><div class="co-thumb"><img src="https://images.unsplash.com/photo-1447933601403-0c6688de566e?w=112&h=112&fit=crop&auto=format&q=70" alt="" width="56" height="56"><span class="badge rounded-pill" aria-hidden="true">2</span></div>
          <div><p class="co-name">Ethiopia Guji, whole bean</p><p class="co-meta">340 g · Light roast<span class="visually-hidden">, quantity 2</span></p></div><p class="co-price">$38.00</p></li>
        <li class="list-group-item co-item"><div class="co-thumb"><img src="https://images.unsplash.com/photo-1517668808822-9ebb02f2a0e6?w=112&h=112&fit=crop&auto=format&q=70" alt="" width="56" height="56"><span class="badge rounded-pill" aria-hidden="true">1</span></div>
          <div><p class="co-name">House espresso, whole bean</p><p class="co-meta">1 kg · Medium-dark roast<span class="visually-hidden">, quantity 1</span></p></div><p class="co-price">$42.00</p></li>
      </ul>
      <dl class="co-rows">
        <div class="co-row"><dt>Subtotal</dt><dd data-subtotal>$80.00</dd></div>
        <div class="co-row"><dt>Shipping (<span data-ship-label>Standard</span>)</dt><dd data-shipping>$5.00</dd></div>
        <div class="co-row"><dt>Estimated tax (7%)</dt><dd data-tax>$5.60</dd></div>
      </dl>
      <dl class="co-total"><dt>Total</dt><dd data-total>$90.60</dd></dl>
      <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>
      <p class="co-fine">Demo checkout. No payment is taken.</p>
      <p class="co-status" role="status" data-status></p>
    </div></aside>
  </div>
</form>
</div>
<div><span class="lbl">Order confirmation</span>
<section class="card co co-done" aria-labelledby="co-done-title"><div class="card-body">
  <div class="co-done-mark"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></div>
  <h2 id="co-done-title">Thanks, Maya. Your order is in.</h2>
  <p class="co-done-lead">Order RC-20417. We roast on Tuesdays and Fridays, so your beans will be at most two days old when they ship.</p>
  <dl class="co-facts">
    <div><dt>Delivery</dt><dd>Standard, 3 to 5 business days</dd></div>
    <div><dt>Ship to</dt><dd>Maya Lindqvist, 2210 SE Clinton St, Portland</dd></div>
    <div><dt>Items</dt><dd>3 items</dd></div>
    <div><dt>Total</dt><dd>$90.60</dd></div>
  </dl>
  <div class="co-done-actions"><a class="btn btn-primary" href="#">Track your order</a><a class="btn btn-outline-primary" href="#">Continue shopping</a></div>
</div></section>
</div>
CSS
/* Bootstrap .row.g-3 grid, .form-control, .form-select, .form-check, .invalid-feedback, .alert, .card and
   .list-group, recoloured through Bootstrap's CSS variables. The card fields are a front-end mock with no name
   attribute; the script stops the demo submit and adds .is-invalid / aria-invalid to fields that fail. */
.co{--co-accent:#7a3e0c;--co-accent-dark:#62310a;--co-ink:#211812;--co-muted:#5f544b;--co-line:#e7e0d8;--co-field:#8b8178;--co-soft:#faf6f1;--co-ring:rgba(122,62,12,.25);--co-error:#b42318;--co-ok:#166534;
  --bs-body-color:var(--co-ink);--bs-secondary-color:var(--co-muted);--bs-border-color:var(--co-field);--bs-border-radius:10px;
  --bs-form-invalid-border-color:var(--co-error);--bs-form-invalid-color:var(--co-error);color:var(--co-ink);font-size:.93rem;line-height:1.45}
.co p,.co h2,.co dl,.co dd,.co ul{margin:0}
.co .btn:focus-visible,.co a:focus-visible{outline:2px solid var(--co-accent);outline-offset:2px;box-shadow:none}
.co-layout{display:grid;grid-template-columns:minmax(0,1fr) 350px;gap:28px;align-items:start}
.co .card{--bs-card-border-width:0;--bs-card-border-radius:18px;box-shadow:0 1px 2px rgba(33,24,18,.05),0 16px 40px -24px rgba(33,24,18,.3)}
.co-main{--bs-card-spacer-x:28px;--bs-card-spacer-y:28px}
.co-brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;font-size:.88rem;margin-bottom:6px!important}
.co-brand span{width:30px;height:30px;border-radius:50%;background:var(--co-accent);color:#fff;display:grid;place-items:center;font-size:.8rem}
.co-note{color:var(--co-muted);font-size:.84rem;margin-bottom:6px!important}
.co-set{padding:22px 0 4px;min-width:0}
.co-set+.co-set{border-top:1px solid var(--co-line);margin-top:18px}
.co-legend{font-size:1.1rem;font-weight:700;margin-bottom:14px;display:flex;align-items:center;gap:10px}
.co-legend i{font-style:normal;width:26px;height:26px;border-radius:50%;background:var(--co-soft);border:1px solid var(--co-line);display:grid;place-items:center;font-size:.78rem;font-weight:700}
.co .row{--bs-gutter-x:14px;--bs-gutter-y:14px}
.co .form-label{font-size:.84rem;font-weight:600;margin-bottom:6px}
.co .form-control,.co .form-select{padding:11px 13px;font-size:inherit;line-height:inherit;color:var(--co-ink);transition:border-color .15s,box-shadow .15s}
.co .form-select{padding-right:38px;cursor:pointer}
.co .form-control::placeholder{color:#7d7268}
.co .form-control:focus,.co .form-select:focus{border-color:var(--co-accent);box-shadow:0 0 0 3px var(--co-ring)}
.co .form-control.is-invalid{background-image:none;padding-right:13px}
.co .form-control.is-invalid:focus{box-shadow:0 0 0 3px rgba(180,35,24,.2)}
.co .form-text{font-size:.78rem;color:var(--co-muted);margin-top:5px}
.co .invalid-feedback{display:flex;gap:6px;font-size:.8rem;font-weight:600;margin-top:6px}
.co .invalid-feedback::before{content:"!";flex:none;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--co-error);color:#fff;font-size:.7rem;font-weight:800;display:grid;place-items:center}
.co .invalid-feedback:empty{display:none}
.co .form-check{display:flex;gap:10px;align-items:flex-start;padding:0;margin:12px 0 0;min-height:0;font-size:.88rem}
.co .form-check-input{float:none;flex:none;width:18px;height:18px;margin:1px 0 0;border-color:var(--co-field);cursor:pointer}
.co .form-check-input:checked{background-color:var(--co-accent);border-color:var(--co-accent)}
.co .form-check-input:focus{box-shadow:none;border-color:var(--co-field)}
.co .form-check-input:focus-visible{outline:2px solid var(--co-accent);outline-offset:2px}
.co-radio{display:grid!important;grid-template-columns:auto minmax(0,1fr) auto;gap:12px!important;padding:14px 16px!important;margin:0!important;border:1px solid var(--co-field);border-radius:12px;cursor:pointer;transition:border-color .15s,background-color .15s}
.co-radio:hover{border-color:var(--co-ink)}
.co-radio:has(:checked){border-color:var(--co-accent);background:var(--co-soft);box-shadow:inset 0 0 0 1px var(--co-accent)}
.co-radio strong{display:block;font-weight:600}
.co-radio span span{font-size:.82rem;color:var(--co-muted)}
.co-radio b{font-variant-numeric:tabular-nums}
.co .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}
.co .alert svg{flex:none;margin-top:1px}
.co .alert strong{display:block}
.co-sum{position:sticky;top:20px;--bs-card-spacer-x:24px;--bs-card-spacer-y:24px}
.co-sum-title{font-size:1.1rem;font-weight:700;margin-bottom:8px!important}
.co .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(--co-ink);
  border-bottom:1px solid var(--co-line);padding-bottom:8px;border-radius:0}
.co-item{display:grid!important;grid-template-columns:56px minmax(0,1fr) auto;gap:12px;align-items:center}
.co-thumb{position:relative}
.co-thumb img{width:56px;height:56px;border-radius:10px;object-fit:cover;background:var(--co-soft)}
.co-thumb .badge{--bs-badge-font-size:.7rem;--bs-badge-padding-x:5px;position:absolute;top:-6px;right:-6px;min-width:21px;height:21px;display:grid;place-items:center;background:var(--co-muted)}
.co-name{font-weight:600;line-height:1.3;font-size:.9rem}
.co-meta{font-size:.8rem;color:var(--co-muted)}
.co-price{font-weight:600;font-variant-numeric:tabular-nums}
.co-rows{display:grid;gap:8px;padding:14px 0;border-bottom:1px solid var(--co-line)}
.co-row{display:flex;justify-content:space-between;gap:12px}
.co-row dt{color:var(--co-muted);font-weight:400}
.co-row dd{font-weight:600;font-variant-numeric:tabular-nums}
.co-total{display:flex;justify-content:space-between;align-items:baseline;padding:14px 0 4px;font-size:1.05rem;font-weight:700}
.co-total dd{font-size:1.35rem;font-weight:800;font-variant-numeric:tabular-nums}
.co .btn-primary{--bs-btn-bg:var(--co-accent);--bs-btn-border-color:var(--co-accent);--bs-btn-hover-bg:var(--co-accent-dark);--bs-btn-hover-border-color:var(--co-accent-dark);
  --bs-btn-active-bg:var(--co-accent-dark);--bs-btn-active-border-color:var(--co-accent-dark);--bs-btn-padding-y:13px;--bs-btn-font-weight:700;--bs-btn-border-radius:12px;
  display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:16px}
.co .btn-outline-primary{--bs-btn-color:var(--co-accent);--bs-btn-border-color:var(--co-accent);--bs-btn-hover-bg:var(--co-soft);--bs-btn-hover-color:var(--co-accent-dark);--bs-btn-hover-border-color:var(--co-accent);
  --bs-btn-active-bg:var(--co-line);--bs-btn-active-color:var(--co-accent-dark);--bs-btn-active-border-color:var(--co-accent)}
.co-fine{font-size:.78rem;color:var(--co-muted);text-align:center;margin-top:10px!important}
.co-status:not(:empty){margin-top:14px!important;padding:12px 14px;border-radius:10px;background:#f0fdf4;color:var(--co-ok);font-weight:600;font-size:.85rem}
.co-done{max-width:560px;--bs-card-spacer-x:30px;--bs-card-spacer-y:30px}
.co-done-mark{width:56px;height:56px;border-radius:50%;background:#e8f5ec;color:var(--co-ok);display:grid;place-items:center;margin-bottom:14px}
.co-done h2{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:1.7rem;line-height:1.15}
.co-done-lead{color:var(--co-muted);margin:6px 0 18px!important}
.co-facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;padding:16px 0;border-block:1px solid var(--co-line);margin-bottom:18px!important}
.co-facts dt{font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;color:var(--co-muted);font-weight:600}
.co-facts dd{font-weight:600;margin-top:2px}
.co-done-actions{display:flex;flex-wrap:wrap;gap:10px}
.co-done-actions .btn{flex:1;min-width:160px;--bs-btn-padding-y:12px;--bs-btn-font-weight:700;--bs-btn-border-radius:12px;margin:0}
@media (max-width:900px){.co-layout{grid-template-columns:1fr}.co-sum{position:static}}
@media (max-width:575px){.co-main{--bs-card-spacer-x:18px;--bs-card-spacer-y:22px}.co-facts{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.co .form-control,.co .form-select,.co-radio,.co .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();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5f544b] uppercase">Single-page checkout</span>
<form class="text-[.93rem] leading-[1.45] text-co-ink" action="#" method="post" data-checkout data-subtotal="80.00" data-tax="7" aria-labelledby="co-title">
  <div class="grid grid-cols-[minmax(0,1fr)_350px] items-start gap-7 max-[900px]:grid-cols-1">
    <div class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(33,24,18,.05),0_16px_40px_-24px_rgba(33,24,18,.3)] p-7 max-[575px]:px-[18px] max-[575px]:py-[22px]">
      <p class="mb-1.5 flex items-center gap-2.5 text-[.88rem] font-extrabold tracking-[.06em] uppercase"><span class="grid size-[30px] place-items-center rounded-full bg-co-accent text-[.8rem] text-white" aria-hidden="true">R</span>Ridgeline Coffee</p>
      <h2 class="sr-only" id="co-title">Checkout</h2>
      <p class="mb-1.5 text-[.84rem] text-co-muted">All fields are required unless marked optional.</p>
      <fieldset class="min-w-0 pt-[22px] pb-1 [&>legend+*]:clear-left [&+&]:mt-[18px] [&+&]:border-t [&+&]:border-co-line"><legend class="float-left mb-3.5 flex w-full items-center gap-2.5 text-[1.1rem] font-bold"><i class="grid size-[26px] place-items-center rounded-full border border-co-line bg-co-soft text-[.78rem] font-bold not-italic" aria-hidden="true">1</i>Contact</legend>
        <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="co-email">Email</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-email" name="email" type="email" autocomplete="email" required aria-describedby="co-email-err" data-missing="Enter your email address." data-bad="Enter an email address like [email protected]."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-email-err"></p></div>
        </div>
        <label class="mt-3 flex cursor-pointer items-start gap-2.5 text-[.88rem]"><input class="mt-px size-[18px] flex-none cursor-pointer accent-co-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-accent" type="checkbox" name="news"><span>Email me when new roasts land (optional)</span></label>
      </fieldset>
      <fieldset class="min-w-0 pt-[22px] pb-1 [&>legend+*]:clear-left [&+&]:mt-[18px] [&+&]:border-t [&+&]:border-co-line"><legend class="float-left mb-3.5 flex w-full items-center gap-2.5 text-[1.1rem] font-bold"><i class="grid size-[26px] place-items-center rounded-full border border-co-line bg-co-soft text-[.78rem] font-bold not-italic" aria-hidden="true">2</i>Shipping address</legend>
        <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="co-country">Country or region</label><select class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none co-chevron cursor-pointer appearance-none bg-[length:16px] bg-[right_12px_center] bg-no-repeat pr-[38px]" id="co-country" name="country" autocomplete="country"><option value="US" selected>United States</option><option value="CA">Canada</option><option value="GB">United Kingdom</option><option value="AU">Australia</option></select></div>
          <div class="col-span-6 min-[576px]:col-span-3"><label class="mb-1.5 block text-[.84rem] font-semibold" for="co-first">First name</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-first" name="first-name" type="text" autocomplete="shipping given-name" required aria-describedby="co-first-err" data-missing="Enter your first name."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-first-err"></p></div>
          <div class="col-span-6 min-[576px]:col-span-3"><label class="mb-1.5 block text-[.84rem] font-semibold" for="co-last">Last name</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-last" name="last-name" type="text" autocomplete="shipping family-name" required aria-describedby="co-last-err" data-missing="Enter your last name."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-last-err"></p></div>
          <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="co-address">Street address</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-address" name="address" type="text" autocomplete="shipping address-line1" required aria-describedby="co-address-err" data-missing="Enter your street address."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-address-err"></p></div>
          <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="co-address2">Apartment, suite, etc. (optional)</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-address2" name="address2" type="text" autocomplete="shipping address-line2"></div>
          <div class="col-span-6 min-[576px]:col-span-2"><label class="mb-1.5 block text-[.84rem] font-semibold" for="co-city">City</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-city" name="city" type="text" autocomplete="shipping address-level2" required aria-describedby="co-city-err" data-missing="Enter your city."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-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="co-state">State</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-state" name="state" type="text" autocomplete="shipping address-level1" required aria-describedby="co-state-err" data-missing="Enter your state."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-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="co-zip">ZIP code</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-zip" name="zip" type="text" autocomplete="shipping postal-code" inputmode="numeric" pattern="[0-9]{5}(-[0-9]{4})?" required aria-describedby="co-zip-err" data-missing="Enter your ZIP code." data-bad="Enter a 5-digit ZIP code, like 97214."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-zip-err"></p></div>
          <div class="col-span-6 min-[576px]:col-span-6"><label class="mb-1.5 block text-[.84rem] font-semibold" for="co-phone">Phone (optional)</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-phone" name="phone" type="tel" autocomplete="shipping tel" aria-describedby="co-phone-hint co-phone-err"><p class="mt-[5px] text-[.78rem] text-co-muted" id="co-phone-hint">Only used to text you about this delivery.</p></div>
        </div>
      </fieldset>
      <fieldset class="min-w-0 pt-[22px] pb-1 [&>legend+*]:clear-left [&+&]:mt-[18px] [&+&]:border-t [&+&]:border-co-line"><legend class="float-left mb-3.5 flex w-full items-center gap-2.5 text-[1.1rem] font-bold"><i class="grid size-[26px] place-items-center rounded-full border border-co-line bg-co-soft text-[.78rem] font-bold not-italic" aria-hidden="true">3</i>Delivery</legend>
        <div class="grid gap-2.5">
          <label class="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-xl border border-co-field px-4 py-3.5 transition-colors duration-150 hover:border-co-ink has-checked:border-co-accent has-checked:bg-co-soft has-checked:shadow-[inset_0_0_0_1px_var(--color-co-accent)] motion-reduce:transition-none"><input class="mt-px size-[18px] cursor-pointer accent-co-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-accent" type="radio" name="delivery" value="std" data-ship-option data-label="Standard" data-price="5.00" checked><span><strong class="block font-semibold">Standard</strong><span class="text-[.82rem] text-co-muted">3 to 5 business days</span></span><b class="tabular-nums">$5.00</b></label>
          <label class="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-xl border border-co-field px-4 py-3.5 transition-colors duration-150 hover:border-co-ink has-checked:border-co-accent has-checked:bg-co-soft has-checked:shadow-[inset_0_0_0_1px_var(--color-co-accent)] motion-reduce:transition-none"><input class="mt-px size-[18px] cursor-pointer accent-co-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-accent" type="radio" name="delivery" value="exp" data-ship-option data-label="Express" data-price="14.00"><span><strong class="block font-semibold">Express</strong><span class="text-[.82rem] text-co-muted">Next business day</span></span><b class="tabular-nums">$14.00</b></label>
          <label class="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 rounded-xl border border-co-field px-4 py-3.5 transition-colors duration-150 hover:border-co-ink has-checked:border-co-accent has-checked:bg-co-soft has-checked:shadow-[inset_0_0_0_1px_var(--color-co-accent)] motion-reduce:transition-none"><input class="mt-px size-[18px] cursor-pointer accent-co-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-accent" type="radio" name="delivery" value="pick" data-ship-option data-label="Pick up at the roastery" data-price="0.00"><span><strong class="block font-semibold">Pick up at the roastery</strong><span class="text-[.82rem] text-co-muted">Ready in 2 hours · 18 Alder Street</span></span><b class="tabular-nums">Free</b></label>
        </div>
      </fieldset>
      <fieldset class="min-w-0 pt-[22px] pb-1 [&>legend+*]:clear-left [&+&]:mt-[18px] [&+&]:border-t [&+&]:border-co-line"><legend class="float-left mb-3.5 flex w-full items-center gap-2.5 text-[1.1rem] font-bold"><i class="grid size-[26px] place-items-center rounded-full border border-co-line bg-co-soft text-[.78rem] font-bold not-italic" aria-hidden="true">4</i>Payment</legend>
        <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>These fields are not connected to a payment provider. Nothing you type is sent or stored and no card is charged. In production, use your provider's secure card fields.</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="co-card">Card number</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-card" type="text" autocomplete="cc-number" inputmode="numeric" pattern="[0-9 ]{13,23}" placeholder="1234 1234 1234 1234" required aria-describedby="co-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-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-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="co-exp">Expiry date</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-exp" type="text" autocomplete="cc-exp" inputmode="numeric" pattern="(0[1-9]|1[0-2]) ?/ ?[0-9]{2}" placeholder="MM / YY" required aria-describedby="co-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-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-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="co-cvc">Security code</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-cvc" type="text" autocomplete="cc-csc" inputmode="numeric" pattern="[0-9]{3,4}" required aria-describedby="co-cvc-hint co-cvc-err" data-missing="Enter the security code." data-bad="Enter the 3 or 4 digit code."><p class="mt-[5px] text-[.78rem] text-co-muted" id="co-cvc-hint">3 or 4 digits on the back.</p><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-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="co-ccname">Name on card</label><input class="block w-full rounded-[10px] border border-co-field bg-white px-[13px] py-[11px] transition-[border-color,box-shadow] duration-150 placeholder:text-[#7d7268] focus:border-co-accent focus:shadow-[0_0_0_3px_rgba(122,62,12,.25)] focus:outline-none aria-invalid:border-co-error aria-invalid:focus:shadow-[0_0_0_3px_rgba(180,35,24,.2)] motion-reduce:transition-none" id="co-ccname" type="text" autocomplete="cc-name" required aria-describedby="co-ccname-err" data-missing="Enter the name on the card."><p class="mt-1.5 flex gap-1.5 text-[.8rem] font-semibold text-co-error empty:hidden before:mt-px before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-co-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="co-ccname-err"></p></div>
        </div>
      </fieldset>
    </div>
    <aside class="sticky top-5 rounded-[18px] bg-white shadow-[0_1px_2px_rgba(33,24,18,.05),0_16px_40px_-24px_rgba(33,24,18,.3)] p-6 max-[900px]:static" aria-labelledby="co-sum-title">
      <h2 class="mb-2 text-[1.1rem] font-bold" id="co-sum-title">Order summary</h2>
      <ul class="border-b border-co-line pb-2">
        <li class="grid grid-cols-[56px_minmax(0,1fr)_auto] items-center gap-3 py-2"><div class="relative"><img class="size-14 rounded-[10px] bg-co-soft object-cover" src="https://images.unsplash.com/photo-1447933601403-0c6688de566e?w=112&h=112&fit=crop&auto=format&q=70" alt="" width="56" height="56"><span class="absolute -top-1.5 -right-1.5 grid h-[21px] min-w-[21px] place-items-center rounded-full bg-co-muted px-[5px] text-[.7rem] font-bold text-white" aria-hidden="true">2</span></div>
          <div><p class="text-[.9rem] leading-[1.3] font-semibold">Ethiopia Guji, whole bean</p><p class="text-[.8rem] text-co-muted">340 g · Light roast<span class="sr-only">, quantity 2</span></p></div><p class="font-semibold tabular-nums">$38.00</p></li>
        <li class="grid grid-cols-[56px_minmax(0,1fr)_auto] items-center gap-3 py-2"><div class="relative"><img class="size-14 rounded-[10px] bg-co-soft object-cover" src="https://images.unsplash.com/photo-1517668808822-9ebb02f2a0e6?w=112&h=112&fit=crop&auto=format&q=70" alt="" width="56" height="56"><span class="absolute -top-1.5 -right-1.5 grid h-[21px] min-w-[21px] place-items-center rounded-full bg-co-muted px-[5px] text-[.7rem] font-bold text-white" aria-hidden="true">1</span></div>
          <div><p class="text-[.9rem] leading-[1.3] font-semibold">House espresso, whole bean</p><p class="text-[.8rem] text-co-muted">1 kg · Medium-dark roast<span class="sr-only">, quantity 1</span></p></div><p class="font-semibold tabular-nums">$42.00</p></li>
      </ul>
      <dl class="grid gap-2 border-b border-co-line py-3.5">
        <div class="flex justify-between gap-3"><dt class="text-co-muted">Subtotal</dt><dd class="font-semibold tabular-nums" data-subtotal>$80.00</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-co-muted">Shipping (<span data-ship-label>Standard</span>)</dt><dd class="font-semibold tabular-nums" data-shipping>$5.00</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-co-muted">Estimated tax (7%)</dt><dd class="font-semibold tabular-nums" data-tax>$5.60</dd></div>
      </dl>
      <dl class="flex items-baseline justify-between pt-3.5 pb-1 text-[1.05rem] font-bold"><dt>Total</dt><dd class="text-[1.35rem] font-extrabold tabular-nums" data-total>$90.60</dd></dl>
      <button class="mt-4 flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-co-accent px-[18px] py-3.5 font-bold text-white transition-colors duration-150 hover:bg-co-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-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>
      <p class="mt-2.5 text-center text-[.78rem] text-co-muted">Demo checkout. No payment is taken.</p>
      <p class="text-[.85rem] font-semibold text-co-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>
    </aside>
  </div>
</form>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5f544b] uppercase">Order confirmation</span>
<section class="max-w-[560px] rounded-[18px] bg-white shadow-[0_1px_2px_rgba(33,24,18,.05),0_16px_40px_-24px_rgba(33,24,18,.3)] p-[30px] text-[.93rem] leading-[1.45] text-co-ink" aria-labelledby="co-done-title">
  <div class="mb-3.5 grid size-14 place-items-center rounded-full bg-[#e8f5ec] text-co-ok"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></div>
  <h2 class="font-[Georgia,'Times_New_Roman',serif] text-[1.7rem] leading-[1.15] font-normal" id="co-done-title">Thanks, Maya. Your order is in.</h2>
  <p class="mt-1.5 mb-[18px] text-co-muted">Order RC-20417. We roast on Tuesdays and Fridays, so your beans will be at most two days old when they ship.</p>
  <dl class="mb-[18px] grid grid-cols-2 gap-3.5 border-y border-co-line py-4 max-[575px]:grid-cols-1 [&_dd]:mt-0.5 [&_dd]:font-semibold [&_dt]:text-[.75rem] [&_dt]:font-semibold [&_dt]:tracking-[.08em] [&_dt]:text-co-muted [&_dt]:uppercase">
    <div><dt>Delivery</dt><dd>Standard, 3 to 5 business days</dd></div>
    <div><dt>Ship to</dt><dd>Maya Lindqvist, 2210 SE Clinton St, Portland</dd></div>
    <div><dt>Items</dt><dd>3 items</dd></div>
    <div><dt>Total</dt><dd>$90.60</dd></div>
  </dl>
  <div class="flex flex-wrap gap-2.5"><a class="flex min-w-40 flex-1 justify-center rounded-xl border border-co-accent bg-co-accent px-4 py-3 font-bold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-accent" href="#">Track your order</a><a class="flex min-w-40 flex-1 justify-center rounded-xl border border-co-accent px-4 py-3 font-bold text-co-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-co-accent" href="#">Continue shopping</a></div>
</section>
</div>
CSS
@theme {
  --color-co-accent: #7a3e0c;
  --color-co-accent-dark: #62310a;
  --color-co-ink: #211812;
  --color-co-muted: #5f544b;
  --color-co-line: #e7e0d8;
  --color-co-field: #8b8178;
  --color-co-soft: #faf6f1;
  --color-co-error: #b42318;
  --color-co-ok: #166534;
}
/* chevron drawn on the native select */
@utility co-chevron {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%235f544b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
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();
});

About this set

A complete one-page checkout for a coffee roaster and the confirmation that follows it. Single-page checkout is one form in four numbered fieldsets: contact email with an optional newsletter tick, a shipping address, delivery options as radio cards for standard, express and pick-up, and a payment section. Beside it a sticky order summary lists the items and recalculates shipping, a 7 percent tax estimate and the total when the delivery choice changes. The payment section is clearly a front-end mock: a notice says it is not connected to a payment provider, the card fields have no name attribute so the form would never submit them, and the button reads Place demo order. Address fields carry the shipping autocomplete tokens, from given-name to postal-code, and the card fields use cc-number, cc-exp, cc-csc and cc-name, so browsers can fill them. On submit a short script checks every field with the browser’s own rules, writes a specific message under each problem, marks it with aria-invalid and moves focus to the first one. Order confirmation shows the thank-you card with the order number, delivery, address and total.

What’s included

  • Contact, shipping address, delivery and payment in one form
  • Delivery radio cards that update shipping and total
  • Checkout autocomplete tokens on every address and card field
  • Demo payment fields with no name attribute and a clear notice
  • Inline error messages with focus moved to the first problem

Accessibility

Every field has a visible label, optional fields say so in words, and each group sits in a fieldset with a legend. Hints and error messages are linked with aria-describedby, and failing fields get aria-invalid; focus moves to the first one on submit. Delivery cards are real radio inputs with a visible focus ring, the summary updates are announced in a polite status region, and transitions stop under prefers-reduced-motion.

Customise it

Change --co-accent, --co-line and --co-field on .co. Subtotal and tax rate are data-subtotal and data-tax on the form; each delivery option carries data-price and data-label. In Bootstrap the field grid is .row with .col-* classes; in Tailwind edit the co-* colours in @theme.