Checkout <mv-checkout>
Tunnel de paiement en plusieurs étapes (adresse → livraison → paiement → récapitulatif) : validation à chaque étape avec messages en ligne et focus sur la première erreur, modes de livraison sous forme de cartes avec dates estimées qui mettent à jour le récapitulatif de commande, saisie de carte formatée (groupes de chiffres, MM/YY, détection Visa/Mastercard/Amex/CB, contrôle de Luhn), PayPal et Apple Pay, adresse de facturation repliable, récapitulatif avec liens « Modifier », conditions générales de vente, un bouton « Payer 157,32 $ » avec état de chargement, refus de carte et confirmation.
Catégorie Blocs Type Bloc (section prête à l’emploi) (<mv-checkout>) Statut stable Installe aussi button , cart , input , like-button , number-field , price , product-card , quantity , rating , progress , checkbox , field , radio , select , stepper Keywords checkout, tunnel, commande, paiement, payment, order, funnel, credit-card, shipping, stepper, form, e-commerce, block
When to use
An online store needs a multi-step checkout from address to shipping, payment and review Card entry must be formatted and checked as the user types, with PayPal and Apple Pay options Shipping method choice must update estimated dates and the order summary in real time
Avoid when
Users are still reviewing items and totals before committing to purchase → use Cart instead A generic multi-step form unrelated to payment only needs progress indicators → use Stepper instead
Obtenir Checkout. Inclus dans toutes les offres : le Pro pack contient les 317 composants, avec la CLI et le serveur MCP utilisés ci-dessous.
Voir les offres
Installation
node scripts/add.mjs checkout --out ./src/marvelous
Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["checkout"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, components/checkout/checkout.js, components/checkout/checkout.css, components/button/button.css, components/cart/cart.js, components/cart/cart.css, components/input/input.css, components/like-button/like-button.js, components/like-button/like-button.css, components/number-field/number-field.js, components/number-field/number-field.css, components/price/price.js, components/price/price.css, components/product-card/product-card.js, components/product-card/product-card.css, components/quantity/quantity.js, components/quantity/quantity.css, core/form.js, components/rating/rating.js, components/rating/rating.css, components/progress/progress.css, components/checkbox/checkbox.css, components/field/field.css, components/radio/radio.css, components/select/select.css, components/stepper/stepper.css, components/stepper/stepper.js.
Utilisation
Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<div id="co-demo" style="width:min(100%,70rem);margin-inline:auto">
<style>
#co-demo .brand { display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:1.75rem; padding-bottom:1.25rem; border-bottom:1px solid var(--mv-border) }
#co-demo .brand b { font-weight:600; letter-spacing:.14em; text-transform:uppercase; font-size:.875rem }
#co-demo .brand span { display:inline-flex; align-items:center; gap:.375rem; color:var(--mv-fg-muted); font-size:.8125rem }
</style>
<div class="brand"><b>Lune Studio</b><span><svg viewBox="0 0 24 24" width="16" height="16" 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> 100% secure checkout</span></div>
<mv-checkout id="co-checkout" data-order-prefix="LS">
<div class="mv-checkout">
<div class="mv-checkout-main">
<mv-stepper>
<ol class="mv-stepper" aria-label="Checkout steps">
<li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Address</span></span></button></li>
<li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Shipping</span></span></button></li>
<li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Payment</span></span></button></li>
<li class="mv-stepper-item"><button class="mv-stepper-trigger"><span class="mv-stepper-indicator"></span><span class="mv-stepper-text"><span class="mv-stepper-title">Review</span></span></button></li>
</ol>
</mv-stepper>
<form class="mv-checkout-form" autocomplete="on">
<!-- 1. Contact + address -->
<section class="mv-checkout-step" data-name="address">
<header class="mv-checkout-step-header">
<h2 class="mv-checkout-step-title">Contact and address</h2>
<p class="mv-checkout-step-description">Already have an account? <a href="#sign-in">Sign in</a> to prefill your details.</p>
</header>
<div class="mv-field-group">
<div class="mv-field">
<label class="mv-label" for="co-email" data-required>Email address</label>
<input class="mv-input" id="co-email" name="email" type="email" required autocomplete="email" placeholder="[email protected] " data-error="Enter a valid email address, e.g. [email protected] ." aria-describedby="co-email-desc">
<p class="mv-field-description" id="co-email-desc">For your order confirmation and tracking updates.</p>
<p class="mv-field-error"></p>
</div>
<label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="newsletter" value="yes"> Get new arrivals and members-only offers (one email a week, max)</label>
</div>
<fieldset class="mv-checkout-section">
<legend class="mv-checkout-section-title">Shipping address</legend>
<div class="mv-field-group">
<div class="mv-field">
<label class="mv-label" for="co-country" data-required>Country</label>
<select class="mv-select" id="co-country" name="country" autocomplete="country-name"><option>United States</option><option>Canada</option><option>United Kingdom</option><option>Australia</option><option>Ireland</option></select>
<p class="mv-field-error"></p>
</div>
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="co-first-name" data-required>First name</label>
<input class="mv-input" id="co-first-name" name="first_name" required autocomplete="given-name" data-error-missing="Enter your first name.">
<p class="mv-field-error"></p>
</div>
<div class="mv-field">
<label class="mv-label" for="co-last-name" data-required>Last name</label>
<input class="mv-input" id="co-last-name" name="last_name" required autocomplete="family-name" data-error-missing="Enter your last name.">
<p class="mv-field-error"></p>
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="co-address" data-required>Address</label>
<input class="mv-input" id="co-address" name="address" required autocomplete="address-line1" placeholder="123 Main St" data-error-missing="Enter your street address.">
<p class="mv-field-error"></p>
</div>
<div class="mv-field">
<label class="mv-label" for="co-address2">Apartment, suite, etc.</label>
<input class="mv-input" id="co-address2" name="address2" autocomplete="address-line2" placeholder="Apt, suite, floor, door code…">
<p class="mv-field-error"></p>
</div>
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="co-zip" data-required>ZIP code</label>
<input class="mv-input" id="co-zip" name="zip" required pattern="[0-9]{5}" data-format="postal" autocomplete="postal-code" placeholder="10012" data-error="ZIP code must be 5 digits.">
<p class="mv-field-error"></p>
</div>
<div class="mv-field">
<label class="mv-label" for="co-city" data-required>City</label>
<input class="mv-input" id="co-city" name="city" required autocomplete="address-level2" data-error-missing="Enter your city.">
<p class="mv-field-error"></p>
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="co-phone">Phone</label>
<input class="mv-input" id="co-phone" name="phone" type="tel" autocomplete="tel" placeholder="555 123 4567" pattern="[0-9 +.]{10,17}" data-error="Enter a 10-digit number, e.g. 555 123 4567." aria-describedby="co-phone-desc">
<p class="mv-field-description" id="co-phone-desc">The carrier may contact you about your delivery.</p>
<p class="mv-field-error"></p>
</div>
</div>
</fieldset>
<div class="mv-checkout-actions">
<a class="mv-checkout-back" href="#cart"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg> Back to cart</a>
<button type="submit" class="mv-button" data-checkout-next>Continue to shipping</button>
</div>
</section>
<!-- 2. Shipping -->
<section class="mv-checkout-step" data-name="shipping">
<header class="mv-checkout-step-header">
<h2 class="mv-checkout-step-title">Shipping method</h2>
<p class="mv-checkout-step-description">Order by 2 PM for same-day dispatch from our Brooklyn studio.</p>
</header>
<fieldset class="mv-choice-group">
<legend class="mv-sr-only">Shipping method</legend>
<label class="mv-choice-card">
<input type="radio" class="mv-radio" name="shipping" value="ground" data-shipping="4.9" data-days="2-3" data-label="UPS Ground" required data-error-missing="Choose a shipping method." checked>
<span class="mv-choice-text"><span class="mv-choice-title">UPS Ground</span><span class="mv-choice-description">Tracked, delivered to your door · <span class="mv-checkout-eta" data-role="eta"></span></span></span>
<span class="mv-radio-meta" data-role="price"></span>
</label>
<label class="mv-choice-card">
<input type="radio" class="mv-radio" name="shipping" value="pickup" data-shipping="3.5" data-days="3-5" data-label="Pickup point" required data-error-missing="Choose a shipping method.">
<span class="mv-choice-text"><span class="mv-choice-title">Pickup point</span><span class="mv-choice-description">12,000+ locations nationwide · <span class="mv-checkout-eta" data-role="eta"></span></span></span>
<span class="mv-radio-meta" data-role="price"></span>
</label>
<label class="mv-choice-card">
<input type="radio" class="mv-radio" name="shipping" value="express" data-shipping="12.9" data-days="1" data-label="FedEx Express" required data-error-missing="Choose a shipping method.">
<span class="mv-choice-text"><span class="mv-choice-title">FedEx Express</span><span class="mv-choice-description">Delivered by 1 PM · <span class="mv-checkout-eta" data-role="eta"></span></span></span>
<span class="mv-radio-meta" data-role="price"></span>
</label>
<label class="mv-choice-card">
<input type="radio" class="mv-radio" name="shipping" value="store" data-shipping="0" data-label="In-store pickup, SoHo" required data-error-missing="Choose a shipping method.">
<span class="mv-choice-text"><span class="mv-choice-title">In-store pickup</span><span class="mv-choice-description">SoHo, New York · 118 Spring St · ready in 2 hours</span></span>
<span class="mv-radio-meta" data-role="price"></span>
</label>
</fieldset>
<div class="mv-checkout-actions">
<button type="button" class="mv-checkout-back" data-checkout-prev><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg> Edit address</button>
<button type="submit" class="mv-button" data-checkout-next>Continue to payment</button>
</div>
</section>
<!-- 3. Payment -->
<section class="mv-checkout-step" data-name="payment">
<header class="mv-checkout-step-header">
<h2 class="mv-checkout-step-title">Payment</h2>
<p class="mv-checkout-step-description">All transactions are encrypted. Your card details are never stored.</p>
</header>
<fieldset class="mv-choice-group" data-layout="grid" style="--mv-choice-min:8.5rem">
<legend class="mv-sr-only">Payment method</legend>
<label class="mv-choice-card" data-variant="badge">
<input type="radio" class="mv-radio" name="payment" value="card" data-label="Credit card" data-submit-label="Pay {total}" checked>
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/></svg><span class="mv-choice-title" style="margin-top:.5rem">Credit card</span><span class="mv-choice-description">Visa, Mastercard, Amex</span>
</label>
<label class="mv-choice-card" data-variant="badge">
<input type="radio" class="mv-radio" name="payment" value="paypal" data-label="PayPal" data-submit-label="Continue with PayPal · {total}">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 7V5a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2"/><path d="M3 6v12a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-3"/></svg><span class="mv-choice-title" style="margin-top:.5rem">PayPal</span><span class="mv-choice-description">Or pay in 4, interest-free</span>
</label>
<label class="mv-choice-card" data-variant="badge">
<input type="radio" class="mv-radio" name="payment" value="applepay" data-label="Apple Pay" data-submit-label="Pay with Apple Pay · {total}">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="2.5"/><path d="M11 18h2"/></svg><span class="mv-choice-title" style="margin-top:.5rem">Apple Pay</span><span class="mv-choice-description">Face ID or Touch ID</span>
</label>
</fieldset>
<fieldset class="mv-checkout-card" data-payment-panel="card">
<legend class="mv-sr-only">Credit card</legend>
<div class="mv-field">
<label class="mv-label" for="co-cc" data-required>Card number</label>
<div class="mv-input-group"><input class="mv-input" id="co-cc" name="card_number" required data-format="card" placeholder="1234 5678 9012 3456" data-error-missing="Enter your card number."></div>
<p class="mv-field-error"></p>
</div>
<div class="mv-checkout-card-row">
<div class="mv-field">
<label class="mv-label" for="co-exp" data-required>Expiration date</label>
<input class="mv-input" id="co-exp" name="expiration" required data-format="expiry" placeholder="MM/YY" data-error-missing="Enter the expiration date.">
<p class="mv-field-error"></p>
</div>
<div class="mv-field">
<label class="mv-label" for="co-cvc" data-required>Security code</label>
<input class="mv-input" id="co-cvc" name="cvc" required data-format="cvc" placeholder="123" aria-describedby="co-cvc-desc" data-error-missing="Enter the 3 digits on the back of your card.">
<p class="mv-field-description" id="co-cvc-desc">3 digits on the back (4 on the front for Amex).</p>
<p class="mv-field-error"></p>
</div>
</div>
<div class="mv-field">
<label class="mv-label" for="co-cardholder" data-required>Name on card</label>
<input class="mv-input" id="co-cardholder" name="cardholder" required autocomplete="cc-name" placeholder="As shown on the card" data-error-missing="Enter the cardholder's name.">
<p class="mv-field-error"></p>
</div>
<p class="mv-checkout-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg> Demo: <code>4242 4242 4242 4242</code> is accepted, <code>4000 0000 0000 0002</code> is declined.</p>
</fieldset>
<div class="mv-checkout-wallet" data-payment-panel="paypal" hidden>
<p>After you continue, you'll be redirected to PayPal to sign in and confirm your payment securely.</p>
</div>
<div class="mv-checkout-wallet" data-payment-panel="applepay" hidden>
<p>The Apple Pay sheet opens when you place your order: confirm with Face ID, Touch ID, or your passcode.</p>
</div>
<label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="billing_same" value="yes" checked data-checkout-toggle="billing"> Billing address same as shipping address</label>
<fieldset class="mv-checkout-section" data-checkout-region="billing" data-when="unchecked" hidden>
<legend class="mv-checkout-section-title">Billing address</legend>
<div class="mv-field-group">
<div class="mv-field">
<label class="mv-label" for="co-b-name" data-required>Full name or company</label>
<input class="mv-input" id="co-b-name" name="billing_name" required autocomplete="billing name">
<p class="mv-field-error"></p>
</div>
<div class="mv-field">
<label class="mv-label" for="co-b-address" data-required>Address</label>
<input class="mv-input" id="co-b-address" name="billing_address" required autocomplete="billing address-line1">
<p class="mv-field-error"></p>
</div>
<div class="mv-field-row">
<div class="mv-field">
<label class="mv-label" for="co-b-zip" data-required>ZIP code</label>
<input class="mv-input" id="co-b-zip" name="billing_zip" required pattern="[0-9]{5}" data-format="postal" autocomplete="billing postal-code" data-error="ZIP code must be 5 digits.">
<p class="mv-field-error"></p>
</div>
<div class="mv-field">
<label class="mv-label" for="co-b-city" data-required>City</label>
<input class="mv-input" id="co-b-city" name="billing_city" required autocomplete="billing address-level2">
<p class="mv-field-error"></p>
</div>
</div>
</div>
</fieldset>
<div class="mv-checkout-actions">
<button type="button" class="mv-checkout-back" data-checkout-prev><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg> Edit shipping</button>
<button type="submit" class="mv-button" data-checkout-next>Review order</button>
</div>
</section>
<!-- 4. Review -->
<section class="mv-checkout-step" data-name="review">
<header class="mv-checkout-step-header">
<h2 class="mv-checkout-step-title">Review and confirm</h2>
<p class="mv-checkout-step-description">One last look before you place your order.</p>
</header>
<dl class="mv-checkout-recap">
<div class="mv-checkout-recap-row"><dt>Contact</dt><dd data-recap="{email}"></dd><button type="button" data-checkout-edit="1">Edit<span class="mv-sr-only"> contact</span></button></div>
<div class="mv-checkout-recap-row"><dt>Ship to</dt><dd data-recap="{first_name} {last_name}|{address}, {address2}|{city} {zip}|{country}"></dd><button type="button" data-checkout-edit="1">Edit<span class="mv-sr-only"> address</span></button></div>
<div class="mv-checkout-recap-row"><dt>Method</dt><dd data-recap="{shipping}"></dd><button type="button" data-checkout-edit="2">Edit<span class="mv-sr-only"> shipping method</span></button></div>
<div class="mv-checkout-recap-row"><dt>Payment</dt><dd data-recap="{payment}|{card_number}"></dd><button type="button" data-checkout-edit="3">Edit<span class="mv-sr-only"> payment</span></button></div>
</dl>
<div class="mv-checkout-alert" role="alert" tabindex="-1" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg><p data-role="message"></p></div>
<label class="mv-choice"><input type="checkbox" class="mv-checkbox" name="terms" value="yes" required data-error-missing="Accept the terms of sale to complete your order."> <span>I agree to the <a href="#terms">terms of sale</a> and the <a href="#privacy">privacy policy</a>.</span></label>
<div class="mv-checkout-actions">
<button type="button" class="mv-checkout-back" data-checkout-prev><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg> Edit payment</button>
<button type="submit" class="mv-button" data-checkout-submit><svg viewBox="0 0 24 24" width="16" height="16" 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><span data-role="label">Pay</span></button>
</div>
<p class="mv-checkout-secure"><svg viewBox="0 0 24 24" width="16" height="16" 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> 3-D Secure payment · Free returns within 30 days</p>
</section>
<div class="mv-checkout-done" hidden>
<span class="mv-checkout-done-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
<h2 class="mv-checkout-done-title">Thanks, <span data-value="first_name"></span>, your order is in!</h2>
<p>Order <strong data-role="order-id"></strong> for <strong data-role="total"></strong>. A confirmation email is on its way to <strong data-value="email"></strong>.</p>
<p>Shipping method: <strong data-value="shipping"></strong>. We'll send you a tracking link as soon as it ships.</p>
<div class="mv-checkout-done-actions">
<a class="mv-button" href="#track">Track my order</a>
<a class="mv-button" data-variant="outline" href="#shop">Continue shopping</a>
</div>
</div>
</form>
</div>
<aside class="mv-checkout-summary" aria-label="Order summary">
<div class="mv-checkout-summary-body">
<h2 class="mv-checkout-summary-title">Your order</h2>
<mv-cart shipping="4.9" shipping-label="UPS Ground" tax-rate="0.2" promo-codes="WELCOME10=10%;THANKS5=5;FREESHIP=shipping">
<div class="mv-cart">
<div class="mv-cart-lines" data-variant="compact">
<mv-product-card data-title="Merino Crewneck Sweater" data-price="89" data-compare-at="112" data-quantity="1">
<article class="mv-product-card" data-variant="compact">
<div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.95 0.012 250)'/%3E%3Cstop offset='1' stop-color='oklch(0.87 0.025 255)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Cellipse cx='200' cy='420' rx='150' ry='18' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='oklch(0.34 0.05 262)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23k)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23sh)'/%3E%3Cpath d='M160 118 Q200 152 240 118 Q200 136 160 118Z' fill='black' fill-opacity='.22'/%3E%3Cpath d='M154 116 Q200 160 246 116' fill='none' stroke='black' stroke-opacity='.12' stroke-width='5'/%3E%3Cpath d='M133 370 Q200 379 267 370 L267 388 Q200 397 133 388 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M64 318 L100 331 L103 316 L68 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M336 318 L300 331 L297 316 L332 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M132 222 L133 380 M268 222 L267 380' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3C/svg%3E" alt=""><span class="mv-product-card-qty" aria-label="Quantity 1">1</span></div>
<div class="mv-product-card-body"><p class="mv-product-card-title">Merino Crewneck Sweater</p><p class="mv-product-card-meta">Midnight blue · M</p></div>
<mv-price value="89" compare-at="112" discount="none" data-size="sm" data-layout="stacked"></mv-price>
</article>
</mv-product-card>
<mv-product-card data-title="Glazed Stoneware Mug" data-price="24.9" data-quantity="2">
<article class="mv-product-card" data-variant="compact">
<div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 145)'/%3E%3Cstop offset='1' stop-color='oklch(0.89 0.03 150)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='392' rx='130' ry='16' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M290 220 q64 0 64 60 q0 60-64 60' fill='none' stroke='oklch(0.7 0.06 150)' stroke-width='22' stroke-linecap='round'/%3E%3Cpath d='M110 170 L290 170 L284 370 Q200 392 116 370 Z' fill='oklch(0.86 0.03 75)'/%3E%3Cpath d='M110 170 L290 170 L287 270 C250 286 236 256 206 280 C176 300 150 266 113 284 Z' fill='oklch(0.7 0.06 150)'/%3E%3Cpath d='M110 170 L290 170 L284 370 Q200 392 116 370 Z' fill='url(%23sh)'/%3E%3Cellipse cx='200' cy='170' rx='90' ry='16' fill='oklch(0.36 0.05 55)'/%3E%3Cellipse cx='200' cy='170' rx='90' ry='16' fill='none' stroke='oklch(0.7 0.06 150)' stroke-width='6' style='filter:brightness(1.1)'/%3E%3C/svg%3E" alt=""><span class="mv-product-card-qty" aria-label="Quantity 2">2</span></div>
<div class="mv-product-card-body"><p class="mv-product-card-title">Glazed Stoneware Mug</p><p class="mv-product-card-meta">Sage · 12 oz</p></div>
<mv-price value="49.8" discount="none" data-size="sm" data-layout="stacked"></mv-price>
</article>
</mv-product-card>
<mv-product-card data-title="Fig Tree Scented Candle" data-price="36" data-quantity="1">
<article class="mv-product-card" data-variant="compact">
<div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.012 20)'/%3E%3Cstop offset='1' stop-color='oklch(0.9 0.03 15)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3ClinearGradient id='gls' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.55'/%3E%3Cstop offset='.3' stop-color='white' stop-opacity='.1'/%3E%3Cstop offset='1' stop-color='white' stop-opacity='.35'/%3E%3C/linearGradient%3E%3CradialGradient id='fg'%3E%3Cstop offset='0' stop-color='oklch(0.95 0.12 90)' stop-opacity='.8'/%3E%3Cstop offset='1' stop-color='oklch(0.9 0.12 80)' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Cellipse cx='200' cy='404' rx='120' ry='14' fill='url(%23fl)' opacity='1'/%3E%3Crect x='120' y='200' width='160' height='200' rx='18' fill='oklch(0.8 0.07 15)'/%3E%3Crect x='120' y='200' width='160' height='200' rx='18' fill='url(%23sh)'/%3E%3Crect x='120' y='200' width='160' height='200' rx='18' fill='url(%23gls)'/%3E%3Cellipse cx='200' cy='214' rx='72' ry='10' fill='oklch(0.95 0.02 85)'/%3E%3Cpath d='M200 214 v-18' stroke='oklch(0.25 0.01 60)' stroke-width='3'/%3E%3Crect x='144' y='270' width='112' height='70' rx='4' fill='oklch(0.97 0.01 90)'/%3E%3Ctext x='200' y='302' text-anchor='middle' font-family='Georgia,serif' font-size='18' fill='oklch(0.3 0.02 60)'%3EFig Tree%3C/text%3E%3Ctext x='200' y='322' text-anchor='middle' font-family='Arial,sans-serif' font-size='9' letter-spacing='2' fill='oklch(0.45 0.02 60)'%3ESOY WAX %C2%B7 6.3 OZ%3C/text%3E%3C/svg%3E" alt=""><span class="mv-product-card-qty" aria-label="Quantity 1">1</span></div>
<div class="mv-product-card-body"><p class="mv-product-card-title">Fig Tree Scented Candle</p><p class="mv-product-card-meta">6.3 oz</p></div>
<mv-price value="36" discount="none" data-size="sm" data-layout="stacked"></mv-price>
</article>
</mv-product-card>
</div>
<div class="mv-cart-summary"></div>
</div>
</mv-cart>
</div>
</aside>
</div>
</mv-checkout>
<script type="module">
const co = document.getElementById("co-checkout");
co.processPayment = async (values) => {
await new Promise((r) => setTimeout(r, 1400));
if ((values.card_number ?? "").replace(/\s/g, "") === "4000000000000002") {
return { ok: false, message: "Your bank declined the payment. You haven't been charged: try another card or PayPal." };
}
return { ok: true };
};
</script>
</div>
API
Attributes
Name Type Default Description stepnumber 1Displayed step (1-based); reflected on every navigation. Moving forward validates the steps in between. currency / localestring USD / en-USFormatting for shipping prices, estimated dates and the pay button. data-order-prefixstring ORDPrefix of the generated order number (when processPayment does not return one). data-formatcard | expiry | cvc | postal On an input: formatting as you type (caret preserved) and dedicated validation. data-error / data-error-missingstring On a control: error message for invalid format / empty field. data-shipping / data-days / data-labelnumber / 'min-max' / string On a shipping radio: cost (0 = “Free”), business-day lead time for the estimated date ([data-role=eta]), summary label. An empty [data-role=price] is filled in. data-submit-labelstring On a payment radio: button label ({total} is replaced), e.g. “Pay {total}”. data-payment-panelstring Panel shown when the radio with the same value is checked; otherwise a <fieldset> is disabled (excluded from validation and data). data-checkout-toggle / data-checkout-region [data-when=unchecked]string Checkbox that reveals a region (billing address). data-recapstring On a review element: template “{first_name} {last_name}|{address}” (| = line break); radios → label, card → “Visa •••• 4242”.
Properties
Name Type Description valuesobject Form data (enabled controls only). totalsobject Totals of the summary mv-cart. processPayment(values, totals) => Promise<{ ok, orderId?, message? }> Payment hook; simulated success by default.
Methods
Name Description goTo(n) / next() / prev()Navigation; returns false when validation fails. validateStep(n)Validates a step, marks errors and returns a boolean. submit()Validates everything, then runs the payment.
Events
Name Description mv-stepdetail: { step, previous, name }. mv-submitBefore payment, cancelable; detail: { values, totals }. mv-completePayment accepted; detail: { orderId, values, totals }. mv-errorPayment declined; detail: { message }.
Content structure
Name Description mv-stepperClickable step trail (only steps already reached). form.mv-checkout-form > section.mv-checkout-stepOne section per step: .mv-checkout-step-title (receives focus), .mv-field fields, .mv-checkout-actions ([data-checkout-next] submit, [data-checkout-prev]). .mv-checkout-recap [data-checkout-edit=n]Review and links to step n. [data-checkout-submit] [data-role=label]Pay button (aria-busy while processing). .mv-checkout-alert [data-role=message]Payment error (role=alert). .mv-checkout-doneConfirmation: [data-role=order-id], [data-role=total], [data-value=field]. aside.mv-checkout-summaryOrder summary with mv-cart (compact lines, promo code, totals); collapses to a total below a 54rem container.
CSS classes
Name Description mv-checkout / -main / -form / -step / -step-header / -step-title / -step-description / -section / -section-title / -actions / -back / -card / -card-row / -brand / -wallet / -recap / -recap-row / -alert / -secure / -hint / -done / -done-icon / -done-title / -done-actions / -summary / -summary-toggle / -summary-body / -summary-title / -etaStructure.
Accessibility
Each step is a group named by its title, which receives focus on arrival (announced as “Step 2 of 4: Shipping method”). Validation when moving to the next step: aria-invalid, message linked via aria-describedby, focus on the first invalid field, error count announced; messages update as you type. Enter submits the step (native form). Unreached steps are disabled in the trail. Detected card brand is announced. Pay button is aria-busy, steps are inert while processing, declines use role=alert, confirmation receives focus. Mobile summary: aria-expanded/aria-controls button.