Checkout

Billing information

We’ll send the receipt here.

Payment method

Payment method

Order summary

Root SEO extension

License
1 website
Unit price
€100
Quantity of

Line total: €100

Total €200

@use 'sprucecss/scss/spruce' as *;

.l-checkout {
  &__inner {
    display: grid;
    gap: spacer-clamp('l', 'xl');
    grid-template-columns: minmax(0, 1fr);

    @include breakpoint('md') {
      grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    }
  }

  &__column {
    display: flex;
    flex-direction: column;
    gap: spacer-clamp('l', 'xl');
  }

  &__title {
    margin-block: 0;
  }

  &__form {
    @include layout-stack('m');
  }

  &__billing,
  &__summary,
  &__payment {
    @include layout-stack('m');
  }

  &__summary {
    block-size: 100%;
    display: flex;
    flex-direction: column;
  }

  // The section heading names the payment options, so the fieldset legend is only for screen readers here.
  &__payment .payment-card-group__legend {
    @include visually-hidden;
  }

  &__total {
    align-items: baseline;
    border-block-start: config('border-width-base', $display) solid color('border');
    display: flex;
    font-size: font-size('h4');
    justify-content: space-between;
    padding-block-start: spacer('s');

    strong {
      color: color('heading');
      font-variant-numeric: tabular-nums;
    }
  }

  &__order {
    @include layout-stack('s');
    margin-block-start: auto;
    padding-block-start: spacer('m');

    .form-group--submit {
      margin-block-start: spacer('m');
    }
  }
}
<div class="l-checkout">
    <div class="container">
        <form class="l-checkout__inner" method="post" action="#" data-checkout>
            <div class="l-checkout__column">
                <div class="l-checkout__billing">
                    <h2 class="l-checkout__title">Billing information</h2>
                    <div class="l-checkout__form">
                        <div class="form-row--mixed">
                            <div class="form-group">
                                <label class="form-label" for="checkout-name">Full name</label>
                                <input class="form-control" type="text" id="checkout-name" name="name" autocomplete="name" required>
                            </div>
                            <div class="form-group">
                                <label class="form-label" for="checkout-email">Email</label>
                                <input class="form-control" type="email" id="checkout-email" name="email" autocomplete="email" spellcheck="false" required aria-describedby="checkout-email-help">
                                <p class="form-description" id="checkout-email-help">We’ll send the receipt here.</p>
                            </div>
                        </div>
                        <div class="form-row--mixed">
                            <div class="form-group">
                                <label class="form-label" for="checkout-country">Country</label>
                                <input class="form-control" type="text" id="checkout-country" name="country" autocomplete="country-name" required>
                            </div>
                            <div class="form-group">
                                <label class="form-label" for="checkout-zip">Postal code</label>
                                <input class="form-control" type="text" id="checkout-zip" name="zip" autocomplete="postal-code" required>
                            </div>
                            <div class="form-group">
                                <label class="form-label" for="checkout-city">City</label>
                                <input class="form-control" type="text" id="checkout-city" name="city" autocomplete="address-level2" required>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="form-label" for="checkout-address">Street address</label>
                            <input class="form-control" type="text" id="checkout-address" name="address" autocomplete="street-address" required>
                        </div>
                    </div>
                </div>
                <div class="l-checkout__payment">
                    <h2 class="l-checkout__title">Payment method</h2>
                    <fieldset class="payment-card-group">
                        <legend class="payment-card-group__legend">Payment method</legend>
                        <label class="payment-card">
                            <input class="form-check__control payment-card__control" type="radio" name="payment-method" value="stripe" aria-describedby="payment-stripe-description" checked>
                            <span class="payment-card__caption">
                                <span class="payment-card__title">Card payment</span>
                                <span class="payment-card__description" id="payment-stripe-description">You’ll be redirected to Stripe to pay with your card.</span>
                            </span>
                                <img class="payment-card__logo" src="/img/ui/stripe-logo.svg" alt="" height="28" width="80">
                        </label>
                        <label class="payment-card">
                            <input class="form-check__control payment-card__control" type="radio" name="payment-method" value="paypal" aria-describedby="payment-paypal-description">
                            <span class="payment-card__caption">
                                <span class="payment-card__title">PayPal</span>
                                <span class="payment-card__description" id="payment-paypal-description">You’ll be redirected to PayPal to complete the payment.</span>
                            </span>
                                <img class="payment-card__logo" src="/img/ui/paypal-logo.svg" alt="" height="28" width="80">
                        </label>
                        <label class="payment-card">
                            <input class="form-check__control payment-card__control" type="radio" name="payment-method" value="transfer" aria-describedby="payment-transfer-description">
                            <span class="payment-card__caption">
                                <span class="payment-card__title">Bank transfer</span>
                                <span class="payment-card__description" id="payment-transfer-description">We’ll email you the bank details. Your order ships after the payment arrives.</span>
                            </span>
                        </label>
                    </fieldset>
                </div>
            </div>
            <div class="l-checkout__column">
                <div class="l-checkout__summary">
                    <h2 class="l-checkout__title">Order summary</h2>
                    <article class="order-card" data-order-card data-unit-price="50">
                        <div class="order-card__thumb" aria-hidden="true">
                            <svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 22v-5M9 8V2M15 8V2M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg>
                        </div>
                        <div class="order-card__data">
                            <h3 class="order-card__title" id="order-item-1">
                                <a class="order-card__link" href="#">WooCommerce SimplePay integration</a>
                            </h3>
                            <dl class="order-card__meta">
                                <div><dt>License</dt><dd>3 websites</dd></div>
                                <div><dt>Unit price</dt><dd>€50</dd></div>
                            </dl>
                        </div>
                        <div class="order-card__actions">
                            <div class="number-group" data-number-group>
                                <button class="btn btn--primary btn--sm btn--icon number-group__decrement" type="button" aria-controls="order-quantity-1" data-step="-1">
                                    <span class="sr-only">Decrease quantity</span>
                                    <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5 12h14"/></svg>
                                </button>
                                <input class="number-group__input" id="order-quantity-1" type="number" inputmode="numeric" name="quantity[1]" value="2" min="1" max="20" aria-labelledby="order-quantity-label-1 order-item-1">
                                <button class="btn btn--primary btn--sm btn--icon number-group__increment" type="button" aria-controls="order-quantity-1" data-step="1">
                                    <span class="sr-only">Increase quantity</span>
                                    <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M5 12h14"/></svg>
                                </button>
                            </div>
                            <span class="sr-only" id="order-quantity-label-1">Quantity of</span>
                            <p class="order-card__total" aria-live="polite"><span class="sr-only">Line total: </span><span data-order-total>€100</span></p>
                            <button class="btn btn--outline-danger btn--sm btn--icon order-card__remove" type="button" aria-describedby="order-item-1" data-order-remove>
                                <span class="sr-only">Remove</span>
                                <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
                            </button>
                        </div>
                    </article>
                    <article class="order-card" data-order-card data-unit-price="100">
                        <div class="order-card__thumb" aria-hidden="true">
                            <svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
                        </div>
                        <div class="order-card__data">
                            <h3 class="order-card__title" id="order-item-2">
                                <a class="order-card__link" href="#">Root SEO extension</a>
                            </h3>
                            <dl class="order-card__meta">
                                <div><dt>License</dt><dd>1 website</dd></div>
                                <div><dt>Unit price</dt><dd>€100</dd></div>
                            </dl>
                        </div>
                        <div class="order-card__actions">
                            <div class="number-group" data-number-group>
                                <button class="btn btn--primary btn--sm btn--icon number-group__decrement" type="button" aria-controls="order-quantity-2" data-step="-1">
                                    <span class="sr-only">Decrease quantity</span>
                                    <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5 12h14"/></svg>
                                </button>
                                <input class="number-group__input" id="order-quantity-2" type="number" inputmode="numeric" name="quantity[2]" value="1" min="1" max="20" aria-labelledby="order-quantity-label-2 order-item-2">
                                <button class="btn btn--primary btn--sm btn--icon number-group__increment" type="button" aria-controls="order-quantity-2" data-step="1">
                                    <span class="sr-only">Increase quantity</span>
                                    <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M5 12h14"/></svg>
                                </button>
                            </div>
                            <span class="sr-only" id="order-quantity-label-2">Quantity of</span>
                            <p class="order-card__total" aria-live="polite"><span class="sr-only">Line total: </span><span data-order-total>€100</span></p>
                            <button class="btn btn--outline-danger btn--sm btn--icon order-card__remove" type="button" aria-describedby="order-item-2" data-order-remove>
                                <span class="sr-only">Remove</span>
                                <svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
                            </button>
                        </div>
                    </article>
                    <p class="sr-only" aria-live="polite" data-order-status></p>
                    <div class="l-checkout__order">
                        <p class="l-checkout__total">Total <strong data-checkout-total>€200</strong></p>
                        <label class="form-check">
                            <input class="form-check__control" type="checkbox" value="true" name="terms" required>
                            <span class="form-label form-check__label">I accept the <a href="#">privacy policy</a> and the <a href="#">terms and conditions</a>.</span>
                        </label>
                        <div class="form-group--submit">
                            <button class="btn btn--primary btn--lg btn--block" type="submit">
                                <svg class="btn__icon" aria-hidden="true" focusable="false" height="24" style="fill-rule:evenodd;clip-rule:evenodd;fill:currentColor;overflow:visible;" viewBox="0 0 24 24" width="24" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
                                    <path d="M24,4.8l0,14.4c0,0.66 -0.235,1.225 -0.705,1.695c-0.47,0.47 -1.035,0.705 -1.695,0.705l-19.2,0c-0.66,0 -1.225,-0.235 -1.695,-0.705c-0.47,-0.47 -0.705,-1.035 -0.705,-1.695l-0,-14.4c-0,-0.66 0.235,-1.225 0.705,-1.695c0.47,-0.47 1.035,-0.705 1.695,-0.705l19.2,0c0.66,0 1.225,0.235 1.695,0.705c0.47,0.47 0.705,1.035 0.705,1.695Zm-21.6,2.4l19.2,0l0,-2.4l-19.2,0l0,2.4Zm0,4.8l0,7.2l19.2,0l0,-7.2l-19.2,0Zm0,7.2l0,-14.4l0,14.4Z"></path>
                                </svg>
                                Pay <span data-checkout-total>€200</span>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>
document.querySelectorAll('[data-checkout]').forEach((checkout) => {
  const format = new Intl.NumberFormat(document.documentElement.lang || 'en', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });

  // Sum the remaining order lines: unit price × quantity.
  const update = () => {
    const total = [...checkout.querySelectorAll('[data-order-card]')].reduce((sum, card) => {
      return sum + Number(card.dataset.unitPrice) * Number(card.querySelector('input[type="number"]').value);
    }, 0);

    checkout.querySelectorAll('[data-checkout-total]').forEach((element) => { element.textContent = format.format(total); });
  };

  checkout.addEventListener('change', update);
  document.addEventListener('order:remove', update);
  update();
});

Checkout

A one-page checkout: billing details, the payment method and an editable order summary with a live total, in a two-column layout.

Anatomy

  • .l-checkout follows the block anatomy: .l-checkout > .container > .l-checkout__inner (the form).
  • .l-checkout__column holds the sections: __billing (the form fields), __payment (Payment Cards) and __summary (Order Cards, the total and the submit button in __order).

Accessibility and Usability

  • Every field has the right type and autocomplete value (name, email, country-name, postal-code, address-level2, street-address), so browsers fill in the address in one go.
  • The required fields and the terms checkbox are required, so the browser validates them before submitting.
  • The payment section keeps its visible heading; the fieldset legend inside is visually hidden, so it's not shown twice but still names the radio group.
  • The submit button repeats the amount ("Pay €200"), so people know what they confirm.

Technical Details

  • The total updates when a quantity changes or a line is removed: ecommerce/checkout.js sums the order lines (it listens to the change and order:remove events of the Order Card). Always recalculate the price on the server.
  • Load the scripts of the Number Group and the Order Card too.
  • Two columns from the md breakpoint; on phones, the summary follows the payment.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.