Checkout blocks

The pages between the cart and the receipt: a one-page checkout, a stepped checkout, a payment method picker and the order confirmation. Each block is one .svelte file built from SvTextInput, SvCountryInput, SvPhoneInput, SvMaskedInput, SvRadioGroup, SvStepper, SvCheckBox and SvButton, with money kept in integer cents and formatted with Intl.NumberFormat.

Installation

Add the whole group, or one block by its name (shown under each demo):

See them first with npx @svgrid/ui try blocks/checkout. No project needed.

Split checkout

Contact email, shipping address, delivery speed and card fields on the left; a sticky order summary with items, subtotal, shipping, tax and total on the right. Pay checks every field, including a Luhn check on the card number and an expiry that is not in the past, then shows the button's loading state. Below 820 px the summary moves above the form and collapses to one row with the total.

Open the live example: Split checkout block (Blocks)

npx @svgrid/ui add checkout-split

Stepped checkout

Shipping, Payment and Review behind an SvStepper. Continue validates only the current step. Review lists the address, phone, delivery speed and card with an Edit link per section; after an edit, the button reads "Save and review" and returns straight to Review.

Open the live example: Stepped checkout block (Blocks)

npx @svgrid/ui add checkout-steps

Payment methods

Four radio cards: Card, Wallet pay, Bank transfer and Invoice. Arrow keys move between them. The form for the chosen method appears below it: card fields, the phone that approves a wallet payment, an IBAN with its check digits verified, or company name, VAT number, country and invoice email for business accounts. Continue validates the chosen method only.

Open the live example: Payment methods block (Blocks)

npx @svgrid/ui add checkout-payment-methods

Order confirmation

The thank-you page: a drawn check mark, the order number with a copy button, a delivery window counted in business days from today, the shipping address, what happens next and the paid summary, with Track order and Continue shopping actions.

Open the live example: Order confirmation block (Blocks)

npx @svgrid/ui add checkout-confirmation

Taking the payment

Card numbers should never reach your own server. The SvMaskedInput card fields in these blocks show the layout; in production, mount your payment provider's hosted fields in their place (the provider's script renders the inputs in its own frame and hands you a token), or skip card fields entirely and redirect the shopper to the provider's payment page. Then delete the card, expiry and cvc checks from the block's validator, since the provider validates its own fields.

The order itself is created in a SvelteKit form action. Compute the total there from the cart, not from anything the browser sent, charge the token, and send the shopper to the confirmation page with a 303:

// src/routes/checkout/+page.server.ts
import { fail, redirect } from '@sveltejs/kit'
import { createOrder, chargeOrder } from '$lib/server/orders'
import type { Actions } from './$types'

export const actions: Actions = {
  pay: async ({ request, locals }) => {
    const form = await request.formData()
    const token = String(form.get('paymentToken') ?? '')
    if (!token) return fail(400, { message: 'Payment details are missing.' })

    const order = await createOrder(locals.cartId, {
      email: String(form.get('email')),
      name: String(form.get('name')),
      address: String(form.get('address')),
      city: String(form.get('city')),
      postal: String(form.get('postal')),
      country: String(form.get('country')),
      delivery: String(form.get('delivery')),
    })
    const paid = await chargeOrder(order, token) // your provider's server SDK
    if (!paid) return fail(402, { message: 'The card was declined. Nothing was charged.' })

    redirect(303, `/checkout/confirmation/${order.number}`)
  },
}

On the page, the split checkout's core calls onPay once the form is valid. Get the token from the hosted fields there and post to the action:

import { deserialize } from '$app/forms'
import { goto } from '$app/navigation'

const co = createSplitCheckout({
  items: data.cart.items,
  onPay: async (values) => {
    const paymentToken = await hostedFields.tokenize() // the provider's client SDK
    const body = new FormData()
    for (const k of ['email', 'name', 'address', 'city', 'postal', 'country', 'delivery'] as const) {
      body.set(k, String(values[k] ?? ''))
    }
    body.set('paymentToken', paymentToken)
    const res = await fetch('?/pay', { method: 'POST', body, headers: { 'x-sveltekit-action': 'true' } })
    const result = deserialize(await res.text())
    if (result.type === 'redirect') return goto(result.location)
    throw new Error(result.type === 'failure' ? String(result.data?.message) : 'Payment failed')
  },
})

A throw keeps the form as it was and clears the loading state. For a redirect provider, the action creates the order and the provider's payment session instead, and ends with redirect(303, session.url); the provider sends the shopper back to your confirmation route.

The stepped checkout works the same way through createSteppedCheckout({ onPlace }), which resolves with the order number, and the payment methods block through createPaymentMethods({ onSave }). The confirmation route loads the order and passes it in:

// src/routes/checkout/confirmation/[number]/+page.server.ts
import { error } from '@sveltejs/kit'
import { getOrder } from '$lib/server/orders'

export async function load({ params, locals }) {
  const order = await getOrder(params.number, locals.user)
  if (!order) error(404, 'Order not found')
  return { order }
}
const done = createOrderConfirmation({
  order: data.order,
  writeText: (t) => navigator.clipboard.writeText(t),
})

Headless

Each block is a styled render of the core in its <script module>: createSplitCheckout, createSteppedCheckout, createPaymentMethods and createOrderConfirmation. The cores own the values, totals, validation and async flows and return prop-getters such as formProps(), summaryToggleProps(), stepperProps(), groupProps() and optionProps(). Take a core alone with npx @svgrid/ui add checkout-split --headless and draw your own markup over it.

See also

Live examples

  • Split checkout block - One-page checkout with SvTextInput, SvCountryInput, a delivery SvRadioGroup and SvMaskedInput card fields beside a sticky order summary. Pay checks every field (Luhn check, expiry not in the past), then shows SvButton loading and a toast.
  • Stepped checkout block - An SvStepper with Shipping, Payment and Review: SvPhoneInput, SvCountryInput, SvRadioGroup and SvMaskedInput validated per step. Review has Edit links that jump back and return to Review; Place order shows a loading state.
  • Payment methods block - Radio cards for Card, Wallet pay, Bank transfer and Invoice with arrow-key navigation; the chosen method's form appears below (SvMaskedInput card, SvPhoneInput, IBAN with a mod-97 check, or VAT number plus SvCountryInput). Continue validates only that method.
  • Order confirmation block - Thank-you page with an inline-SVG check mark, a copyable order number, a delivery window counted in business days, next steps, the paid summary with an SvBadge status, and Track order / Continue shopping SvButtons.