Checkout

Checkout form patterns with single-page, split layout, and multi-step flows. Accessible and secure payment form designs.

Overview

Checkout forms are critical for e-commerce success. These patterns demonstrate best practices for collecting shipping and payment information while maintaining user trust and a smooth experience using Vanilla Breeze components.

Key features:

  • <fieldset> with <legend> for semantic grouping of related fields
  • <form-field> for consistent input styling and validation
  • data-layout="sidebar" with <main> and <aside> for order summary placement
  • data-layout="split" for side-by-side fields (city/state, expiry/CVC)
  • data-layout="cluster" for inline elements (security note, summary rows)
  • <icon-wc name="lock"> for security indicators
  • Proper autocomplete attributes for address and payment autofill
  • inputmode="numeric" for mobile keyboard optimization

Single-Page Checkout

A complete checkout form on a single page, using <fieldset> to group contact, shipping, and payment sections. Centered with data-layout="center" and wrapped in a <layout-card> for visual containment. Ideal for simpler purchases or when you want to minimize steps.

<body> <main data-layout="center" data-layout-max="narrow"> <layout-card data-padding="l"> <form action="/checkout" method="POST" data-layout="stack" data-layout-gap="l"> <header data-layout="stack" data-layout-gap="s"> <h1>Checkout</h1> <p>Complete your purchase by providing the information below.</p> </header> <!-- Contact Information --> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Contact information</legend> <form-field> <label for="email">Email address</label> <input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com" aria-describedby="email-error"/> <output id="email-error" class="error" for="email" aria-live="polite"> Please enter a valid email address. </output> </form-field> </fieldset> <!-- Shipping Address --> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Shipping address</legend> <form-field> <label for="full-name">Full name</label> <input type="text" id="full-name" name="full_name" required autocomplete="name" placeholder="John Doe"/> </form-field> <form-field> <label for="address">Street address</label> <input type="text" id="address" name="address" required autocomplete="street-address" placeholder="123 Main St"/> </form-field> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="city">City</label> <input type="text" id="city" name="city" required autocomplete="address-level2" placeholder="San Francisco"/> </form-field> <form-field> <label for="state">State / Province</label> <input type="text" id="state" name="state" required autocomplete="address-level1" placeholder="CA"/> </form-field> </div> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="zip">ZIP / Postal code</label> <input type="text" id="zip" name="zip" required autocomplete="postal-code" placeholder="94102"/> </form-field> <form-field> <label for="country">Country</label> <select id="country" name="country" required autocomplete="country-name"> <option value="">Select country</option> <option value="US" selected>United States</option> <option value="CA">Canada</option> <option value="GB">United Kingdom</option> </select> </form-field> </div> </fieldset> <!-- Payment Information --> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Payment</legend> <form-field> <label for="card-number">Card number</label> <input type="text" id="card-number" name="card_number" required autocomplete="cc-number" placeholder="1234 5678 9012 3456" inputmode="numeric"/> </form-field> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="expiry">Expiration date</label> <input type="text" id="expiry" name="expiry" required autocomplete="cc-exp" placeholder="MM / YY" inputmode="numeric"/> </form-field> <form-field> <label for="cvc">CVC</label> <input type="text" id="cvc" name="cvc" required autocomplete="cc-csc" placeholder="123" inputmode="numeric" maxlength="4"/> </form-field> </div> <p class="security-note" data-layout="cluster" data-layout-gap="xs" data-layout-align="center"> <icon-wc name="lock"></icon-wc> Your payment information is encrypted and secure. </p> </fieldset> <button type="submit">Place order</button> </form> </layout-card> </main> </body>

Fieldset Styles

VB provides native <fieldset> styling (border, radius, padding, legend formatting). These overrides increase the padding and legend font size for a more spacious checkout layout:

/* Override VB native fieldset padding and legend size */ fieldset { padding: var(--size-l); } fieldset legend { font-size: var(--font-size-base); }

Split Layout with Order Summary

A two-column layout using data-layout="sidebar" with the checkout form in a <main> and an order summary <aside>. The sidebar layout automatically recognizes these semantic elements for content vs sidebar sizing. Order items use data-layout="cluster" for label/price pairs and data-layout="stack" for item details. Native <hr> elements provide visual dividers.

<div data-layout="sidebar" data-layout-gap="xl" data-layout-content-min="60"> <!-- Checkout Form --> <main data-layout="stack" data-layout-gap="l"> <header data-layout="stack" data-layout-gap="s"> <h1>Checkout</h1> <p>Complete your purchase by providing shipping and payment information.</p> </header> <form action="/checkout" method="POST" data-layout="stack" data-layout-gap="l"> <!-- Shipping Address --> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Shipping address</legend> <form-field> <label for="email">Email address</label> <input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com"/> </form-field> <form-field> <label for="full-name">Full name</label> <input type="text" id="full-name" name="full_name" required autocomplete="name" placeholder="John Doe"/> </form-field> <form-field> <label for="address">Street address</label> <input type="text" id="address" name="address" required autocomplete="street-address" placeholder="123 Main St"/> </form-field> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="city">City</label> <input type="text" id="city" name="city" required autocomplete="address-level2" placeholder="San Francisco"/> </form-field> <form-field> <label for="state">State</label> <input type="text" id="state" name="state" required autocomplete="address-level1" placeholder="CA"/> </form-field> </div> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="zip">ZIP code</label> <input type="text" id="zip" name="zip" required autocomplete="postal-code" placeholder="94102"/> </form-field> <form-field> <label for="country">Country</label> <select id="country" name="country" required> <option value="">Select country</option> <option value="US" selected>United States</option> <option value="CA">Canada</option> </select> </form-field> </div> </fieldset> <!-- Payment Information --> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Payment</legend> <form-field> <label for="card-number">Card number</label> <input type="text" id="card-number" name="card_number" required autocomplete="cc-number" placeholder="1234 5678 9012 3456" inputmode="numeric"/> </form-field> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="expiry">Expiration</label> <input type="text" id="expiry" name="expiry" required autocomplete="cc-exp" placeholder="MM / YY"/> </form-field> <form-field> <label for="cvc">CVC</label> <input type="text" id="cvc" name="cvc" required autocomplete="cc-csc" placeholder="123" maxlength="4"/> </form-field> </div> <p class="security-note" data-layout="cluster" data-layout-gap="xs" data-layout-align="center"> <icon-wc name="lock"></icon-wc> Your payment information is encrypted and secure. </p> </fieldset> <button type="submit">Place order</button> </form> </main> <!-- Order Summary Sidebar --> <aside class="order-summary" data-layout="stack" data-layout-gap="l"> <h3>Order summary</h3> <div data-layout="stack" data-layout-gap="m"> <div data-layout="cluster" data-layout-justify="between" data-layout-align="start" data-layout-gap="m"> <div data-layout="stack" data-layout-gap="2xs"> <span class="order-item-name">Premium Wireless Headphones</span> <span class="order-item-details">Black, Qty: 1</span> </div> <span class="order-item-price">$249.00</span> </div> <!-- More items... --> </div> <hr/> <div data-layout="stack" data-layout-gap="s"> <div class="summary-row" data-layout="cluster" data-layout-justify="between"> <span>Subtotal</span> <span>$327.98</span> </div> <div class="summary-row" data-layout="cluster" data-layout-justify="between"> <span>Shipping</span> <span>$9.99</span> </div> <div class="summary-row" data-layout="cluster" data-layout-justify="between"> <span>Tax</span> <span>$27.12</span> </div> </div> <hr/> <div class="summary-row total" data-layout="cluster" data-layout-justify="between"> <span>Total</span> <span>$365.09</span> </div> </aside> </div>

Order Summary Styles

Layout is handled entirely by data-layout attributes. These styles cover only visual concerns (font sizes, weights, and colors):

.order-summary h3 { font-size: var(--font-size-lg); margin: 0; } .order-summary hr { margin-block: 0; } .order-item-name { font-weight: var(--font-weight-medium); } .order-item-details { font-size: var(--font-size-sm); color: var(--color-text-muted); } .order-item-price { font-weight: var(--font-weight-medium); white-space: nowrap; } .summary-row { font-size: var(--font-size-sm); color: var(--color-text-muted); } .summary-row.total { font-size: var(--font-size-base); font-weight: var(--font-weight-semibold); color: var(--color-text); }

Multi-Step Checkout

A wizard-style checkout flow with a visual step indicator. Breaks the checkout into Shipping, Payment, and Review steps. Uses data-layout="cluster" for the progress indicator. Reduces cognitive load by focusing on one section at a time.

<body> <main data-layout="center" data-layout-max="narrow"> <layout-card data-padding="l"> <div data-layout="stack" data-layout-gap="l"> <header data-layout="stack" data-layout-gap="s"> <h1>Checkout</h1> <p>Step 1 of 3: Shipping</p> </header> <!-- Step Indicator — uses nav.steps from VB core --> <nav class="steps" aria-label="Checkout progress"> <ol> <li aria-current="step">Shipping</li> <li>Payment</li> <li>Review</li> </ol> </nav> <!-- Step 1: Shipping --> <form action="/checkout/shipping" method="POST" data-layout="stack" data-layout-gap="l"> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Contact information</legend> <form-field> <label for="email">Email address</label> <input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com"/> </form-field> <form-field> <label for="phone">Phone number</label> <input type="tel" id="phone" name="phone" autocomplete="tel" placeholder="(555) 123-4567"/> </form-field> </fieldset> <fieldset data-layout="stack" data-layout-gap="m"> <legend>Shipping address</legend> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="first-name">First name</label> <input type="text" id="first-name" name="first_name" required autocomplete="given-name" placeholder="John"/> </form-field> <form-field> <label for="last-name">Last name</label> <input type="text" id="last-name" name="last_name" required autocomplete="family-name" placeholder="Doe"/> </form-field> </div> <form-field> <label for="address">Street address</label> <input type="text" id="address" name="address" required autocomplete="street-address" placeholder="123 Main St"/> </form-field> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="city">City</label> <input type="text" id="city" name="city" required autocomplete="address-level2" placeholder="San Francisco"/> </form-field> <form-field> <label for="state">State</label> <select id="state" name="state" required autocomplete="address-level1"> <option value="">Select state</option> <option value="CA" selected>California</option> </select> </form-field> </div> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="zip">ZIP code</label> <input type="text" id="zip" name="zip" required autocomplete="postal-code" placeholder="94102"/> </form-field> <form-field> <label for="country">Country</label> <select id="country" name="country" required> <option value="US" selected>United States</option> <option value="CA">Canada</option> </select> </form-field> </div> </fieldset> <div data-layout="cluster" data-layout-justify="between" data-layout-gap="m"> <a href="/cart" class="button secondary">Back to cart</a> <button type="submit">Continue to payment</button> </div> </form> </div> </layout-card> </main> </body>

Step Indicator

The step indicator uses nav.steps from VB core — no custom CSS needed. See the Steps pattern for all variants and customization.

/* No custom CSS needed — nav.steps is included in VB core. Numbered circles, connectors, and checkmarks are automatic. See the Steps pattern page for all variants and CSS variables. */

Configuration

Key configuration options for checkout forms:

Element/Attribute Purpose Options
fieldset + legend Group related fields semantically Contact, Shipping, Payment sections
data-layout="split" Side-by-side form fields City/State, Expiry/CVC pairs
data-layout="sidebar" Two-column layout with order summary Uses <main> (content) + <aside> (sidebar)
inputmode="numeric" Mobile numeric keyboard Card number, CVC, ZIP code
autocomplete Enable browser/payment autofill cc-number, cc-exp, cc-csc, street-address
maxlength Limit input length CVC: 4, ZIP: 10

Usage Notes

PCI Compliance

  • Never store raw credit card numbers on your server
  • Use a payment processor (Stripe, Braintree, etc.) with hosted fields or tokenization
  • These patterns show the form structure; actual payment handling requires integration with a PCI-compliant payment gateway
  • Display security indicators (lock icon, "encrypted" messaging) to build user trust

Address Autofill

  • Use proper autocomplete values: street-address, address-level2 (city), address-level1 (state), postal-code, country-name
  • Consider integrating address verification services (Google Places, SmartyStreets) for accuracy
  • Support international addresses with flexible field requirements

Same Billing/Shipping Address

  • Add a checkbox "Billing address same as shipping" to reduce form fields
  • Show/hide billing address section based on checkbox state
  • Pre-populate billing fields when checkbox is unchecked

Form Validation

  • Validate card numbers with Luhn algorithm
  • Check expiration date is in the future
  • Validate ZIP/postal codes based on selected country
  • Show inline validation errors using <output class="error">

Accessibility

  • <fieldset> and <legend> provide semantic grouping for screen readers
  • All inputs have associated labels
  • Error messages linked via aria-describedby
  • Step indicator uses aria-current="step" for current step
  • Progress is announced with descriptive step text (e.g., "Step 1 of 3: Shipping")

Related

Steps

Step indicator CSS pattern used in multi-step checkout

Registration

Sign up forms with multi-step flows

Form Field

Form field element with validation

Layout Sidebar

Sidebar layout for split designs

Icon

Icon component for security indicators