Skip to content

Checkout

The checkout: one section open at a time, the others collapsed to a summary or waiting their turn, and the summary aside. The site supplies the section set and composes each section's body. Render it on the client once the cart has loaded.

import { Checkout } from "@evolve-storefront/feature-cart/checkout";

Renders inside CartProvider.

Preview

DefaultOpen in Storybook ↗

Usage

A guest from the top: each section opens after the one above is confirmed.

const sections = [
{ id: "details", title: t("details"), isComplete: (cart) => Boolean(cart.customerEmail) },
{ id: "delivery", title: t("delivery"), isComplete: (cart) => Boolean(cart.selectedShippingMethod),
fingerprint: (cart) => ({ country: cart.shippingAddress?.country ?? "" }) },
{ id: "payment", title: t("payment"), isComplete: (cart) => Boolean(cart.selectedPaymentMethod) },
] as const;
<Checkout cart={cart} sections={sections}>
<CheckoutMain>
<CheckoutSection sectionId="details" hint={t("details-hint")} summary={cart.customerEmail}>
<DetailsFields />
<CheckoutSectionContinue onContinue={saveDetails} />
</CheckoutSection>
…
<CheckoutSection sectionId="payment" notice={paymentFailed ? t("payment-failed") : null}>
<PaymentMethods />
<CheckoutPlaceOrder amount={formatMoney(cart.total.gross, locale)} onPlaceOrder={placeOrder} />
</CheckoutSection>
</CheckoutMain>
<CheckoutAside><CartSummary cart={cart} /></CheckoutAside>
</Checkout>;

Props

PropTypeDescription
cartCheckoutCartBase & AnalyticsSourceCart

The cart being checked out. Sections read whether they are complete from it.

sectionsreadonly CheckoutSectionDefinition<CheckoutCartBase>[]

The site's section set, in page order. The last one holds CheckoutPlaceOrder.

childrenReactNode

CheckoutMain with one CheckoutSection per definition, and CheckoutAside.

confirmations?CheckoutConfirmationStore

Where confirmed sections survive a reload. localStorage per cart by default.

layout?CheckoutLayout

single shows every section on one page; multi shows one at a time, with CheckoutStepper above it and the confirmed ones as a recap. Same sections either way.

Any other props go to the root element, like on a plain HTML element.

Change the layout

Pass children to Checkout to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-cart/checkout.

const sections = [
{ id: "details", title: t("details"), isComplete: (cart) => Boolean(cart.customerEmail) },
{ id: "delivery", title: t("delivery"), isComplete: (cart) => Boolean(cart.selectedShippingMethod),
fingerprint: (cart) => ({ country: cart.shippingAddress?.country ?? "" }) },
{ id: "payment", title: t("payment"), isComplete: (cart) => Boolean(cart.selectedPaymentMethod) },
] as const;
<Checkout cart={cart} sections={sections}>
<CheckoutMain>
<CheckoutSection sectionId="details" hint={t("details-hint")} summary={cart.customerEmail}>
<DetailsFields />
<CheckoutSectionContinue onContinue={saveDetails} />
</CheckoutSection>
…
<CheckoutSection sectionId="payment" notice={paymentFailed ? t("payment-failed") : null}>
<PaymentMethods />
<CheckoutPlaceOrder amount={formatMoney(cart.total.gross, locale)} onPlaceOrder={placeOrder} />
</CheckoutSection>
</CheckoutMain>
<CheckoutAside><CartSummary cart={cart} /></CheckoutAside>
</Checkout>;
CheckoutAsideThe summary beside the sections from lg up, sticky so the total stays in view.

No props of its own.

CheckoutBackToCartLabelWhat goes inside the site's back link: an arrow, and "Back to cart", which only the arrow stands in for on narrow screens.

No props of its own.

CheckoutBusinessPurchase"I'm ordering for a business": a box that, ticked, asks for the company name and an optional VAT number.
PropTypeDescription
valueCheckoutBusinessPurchaseValue
onValueChange(value: CheckoutBusinessPurchaseValue) => void
country?string

The billing country, for a VAT number typed without its prefix.

vatCheck?boolean

Check the VAT number live against the register. Needs the framework's VIES module.

companyError?string

Shown under the company name after a submit without one.

Any other props go to the root element, like on a plain HTML element.

CheckoutCarrierWho delivers: the carrier's logo, with its name as alt text, or its name alone when the key has no logo or the image fails to load.
PropTypeDescription
carrierCarrier
logos?Readonly<Record<string, string>>

Any other props go to the root element, like on a plain HTML element.

CheckoutDeliveryEstimateWhen a method arrives: "today", "tomorrow", a date or a range.
PropTypeDescription
estimateMethodEstimate | null

From methodEstimate; renders nothing while it is null.

timeZone?string

The zone "today" and the order-before time are in, the store's. The shopper's own by default.

Any other props go to the root element, like on a plain HTML element.

CheckoutDeliverySectionDelivery: the address and how it is delivered, with the cart's method, or the first on offer, preselected so confirming costs one click.
PropTypeDescription
cartCart
hint?ReactNode

What an upcoming section will ask, beside its title.

notice?ReactNode

One line above the open body: a failed payment, or why the section reopened.

headingLevel?HeadingLevel
sectionId?string

Defaults to delivery, the id b2cCheckoutSections gives it.

detailsSectionId?string

The section whose confirmed name and phone go on the address.

before?ReactNode

Above the address: a home-or-pickup choice.

deliveryOptions?ReactNode

Under the methods: anything else the delivery needs.

slots?boolean

Ask for a delivery date and time slot instead of a method; the slot sets the method. Needs the framework's delivery-options module.

requestedDate?boolean

Ask whether backordered lines ship complete or split, and for an optional requested delivery date. Needs delivery preferences in the framework's delivery-options module; not with slots, whose slot already fixes the day.

timeZone?string

The zone slot windows are shown in, the store's. The shopper's own by default.

pickup?boolean

Offer collecting from a pickup point beside home delivery. Needs the framework's delivery-options module with a pickup provider.

defaultCountry?string

The country a postal-code search for pickup points is in, without an address.

warehouse?CheckoutWarehouse

Offer collecting at the seller's warehouse: no address is asked, and its shipping method is set. Its key is never offered as a home delivery method.

isSignedIn?boolean

Whether the shopper is signed in; only then can an address be saved.

saveToAddressBook?boolean

Offer "Save in my address book" for a newly typed address, ticked by default.

onSaveAddress?CheckoutSaveAddress

Saves the address once the cart has it, when the shopper left the box ticked.

savedAddresses?readonly CartDeliveryAddress[]

The signed-in shopper's address book, offered as cards above "Another address". The cart's address is preselected when it is one of them; without one, the default. A typed address already in it is not saved again.

renderAddressPicker?CheckoutAddressPickerRenderer

Pick from the company's addresses instead of typing one, such as feature-organization's CompanyAddressPicker.

allowOneOffAddress?boolean

With a picker: whether the buyer may type a one-off address instead.

estimates?boolean

Show each method's carrier and when it arrives. Needs the framework's delivery-options module; not with slots, whose slot already says both.

carrierLogos?Readonly<Record<string, string>>

Logo URLs by carrier key, for the methods with estimates, the slots and the pickup carrier chips. A carrier without one shows its name.

companyProfileHref?string

The company profile, linked from the VAT outcome when the company lacks a VAT number.

Any other props go to the root element, like on a plain HTML element.

CheckoutDeliverySlotsWhen to deliver: a chip per date with its lowest price, then the slots on the chosen date with what each costs extra and its carrier.
PropTypeDescription
datesreadonly DeliveryDate[]
valueDeliverySlot | null

The chosen slot.

onValueChange(slot: DeliverySlot) => void
timeZone?string

The zone a window is shown in. The shopper's own by default.

carrierLogos?Readonly<Record<string, string>>

Logo URLs by carrier key. A carrier without one is named under the window.

Any other props go to the root element, like on a plain HTML element.

CheckoutDetailsSectionDetails: the email for the confirmation, and the recipient's name and phone.
PropTypeDescription
cartCart
hint?ReactNode

What an upcoming section will ask, beside its title.

notice?ReactNode

One line above the open body: a failed payment, or why the section reopened.

headingLevel?HeadingLevel
sectionId?string

Defaults to details, the id b2cCheckoutSections gives it.

account?ReactNode

Under the email: the site's account detection and sign-in.

onEmailSettled?((email: string) => void)

The email field lost focus holding a valid address, for account detection.

Any other props go to the root element, like on a plain HTML element.

CheckoutFinishSectionFinishing a business checkout: the purchase reference, then ordering or another ending such as a quote, preselected from the cart button and switchable here.
PropTypeDescription
cartCart
onPlaceOrder(cart: Cart) => unknown

Places the order once the reference, billing and method are on the cart.

hint?ReactNode

What an upcoming section will ask, beside its title.

notice?ReactNode

One line above the open body: a failed payment, or why the section reopened.

headingLevel?HeadingLevel
sectionId?string

Defaults to finish, the id b2bCheckoutSections gives it.

requirePurchaseReference?boolean

The company requires a purchase reference, so the field says so and checks it.

fields?ReactNode

Under the reference: the site's own fields, such as a cost center.

endings?readonly CheckoutEnding[]

The ways to finish beside ordering, such as useQuoteRequestConclusion()'s.

defaultEnding?string

The ending chosen on arrival: the cart button the buyer clicked. Ordering by default.

placeOrderError?unknown
placeOrderBlockedReason?ReactNode

Why the order cannot be placed, such as CartMinimumOrderReason below the minimum order value: the order button is off and says why. Another ending, such as a quote, stays available.

amount?string

What the order button shows. The cart's gross total by default.

billingAddress?CartShippingAddressInput

The invoice address, usually the company's. The cart's own billing or shipping address without it.

renderMethodIcon?((method: PaymentMethod) => ReactNode)
terms?ReactNode

Under the order button: the business terms.

device?ClientDevice

Any other props go to the root element, like on a plain HTML element.

CheckoutGiftCardsGift cards in Payment: number and PIN, a balance check, then "Use".
PropTypeDescription
cartCart
pendingCheckoutPendingGiftCard | null

The card that pays the rest once the order is placed.

onPendingChange(pending: CheckoutPendingGiftCard | null) => void
error?unknown

Why the pending card failed when the order was placed.

Any other props go to the root element, like on a plain HTML element.

CheckoutHeaderThe checkout's own header, in three regions: back to the cart, logo, and an end slot.
PropTypeDescription
backToCart?ReactNode

The way back, the site's link: <Link href="/cart"><CheckoutBackToCartLabel /></Link>.

logo?ReactNode

The centre region, usually the site's logo.

children?ReactNode

The end region: trust cues such as "Veilig afrekenen", or a company switcher.

Any other props go to the root element, like on a plain HTML element.

CheckoutMainThe column of sections, and whatever a site puts above them: an identity block, express checkout.

No props of its own.

CheckoutPaymentSectionPayment: who gets the invoice and how they pay, with the final button in the flow.
PropTypeDescription
cartCart
onPlaceOrder(cart: Cart) => unknown

Places the order once billing and the method are on the cart: place, pay and route. Return false, or throw, to stay on the page.

hint?ReactNode

What an upcoming section will ask, beside its title.

notice?ReactNode

One line above the open body: a failed payment, or why the section reopened.

headingLevel?HeadingLevel
sectionId?string

Defaults to payment, the id b2cCheckoutSections gives it.

detailsSectionId?string

The section whose confirmed name and phone go on the billing address.

placeOrderError?unknown

The place-order mutation's error, shown under the button.

amount?string

What the button shows. The cart's gross total by default.

business?ReactNode

Under the billing address: an "I'm ordering for a business" block.

giftCards?boolean

Offer gift cards above the methods. Needs the framework's gift card module, plus onPayRest and onPaidInFull; without them the section throws.

onPayRest?((cart: Cart, paymentMethod: PaymentMethodInput) => unknown)

Called instead of onPlaceOrder once a gift card is on the cart: pay the rest against the cart (usePayCart) and route. Settling a cart takes the cards' holds on every framework version; placing the order first leaves them untaken on older ones. Return false, or throw, to stay.

onPaidInFull?((order: { id: string; orderNumber: string; }) => unknown)

Called instead of onPlaceOrder when gift cards pay the whole order, with the order paying them made: route to its confirmation. Return false to stay.

renderMethodIcon?((method: PaymentMethod) => ReactNode)

A method's logo, by method.

terms?ReactNode

Under the button: the terms.

device?ClientDevice
deliverySectionId?string

The section whose confirmed choice says whether the parcel goes to a pickup point.

pickup?boolean

The checkout offers pickup points, so a reload can tell from the cart.

warehouseMethodKey?string

Delivery's warehouse.shippingMethodKey, so a reload can tell a collected cart.

businessPurchase?boolean

Offer "I'm ordering for a business", with the company name on the invoice.

vatCheck?boolean

Check the business VAT number live. Needs the framework's VIES module.

isSignedIn?boolean

Whether the shopper is signed in; only then can an address be saved.

saveToAddressBook?boolean

Offer "Save in my address book" for a newly typed billing address, ticked by default.

onSaveAddress?CheckoutSaveAddress

Saves the billing address once the cart has it, when the shopper left the box ticked.

savedAddresses?readonly CartDeliveryAddress[]

The signed-in shopper's address book, offered as cards when billing is not the delivery address. A typed billing address already in it is not saved again.

Any other props go to the root element, like on a plain HTML element.

CheckoutPickupPointsWhere to collect: a postal-code search, a chip per carrier with how many points it has, and the points nearest first, each with its distance, today's hours, earliest pickup and price.
PropTypeDescription
pointsreadonly PickupPoint[]
valuePickupPoint | null
onValueChange(point: PickupPoint) => void
querystring

The postal code searched near.

onSearch(postalCode: string) => void
isPending?boolean
timeZone?string

The zone opening hours and pickup times are shown in, the store's.

carrierLogos?Readonly<Record<string, string>>

Logo URLs by carrier key, for the carrier chips. A carrier without one is named.

Any other props go to the root element, like on a plain HTML element.

CheckoutPlaceOrderThe final button, in the last section's flow.
PropTypeDescription
isPending?boolean

The action is running. The button keeps its label, so its width and accessible name hold, shows a spinner and is aria-busy. It stays focusable rather than disabled, so focus is not dropped mid-save; clicks and the form submit it would trigger are ignored. Not supported with asChild. The order is being placed, for a submit button whose form owns the save.

tone?ButtonTone
isIconOnly?boolean

The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that.

More

Not inferred from the children. A kebab menu is as often a glyph or an SVG as an <Icon>, and a visually-hidden label — the accessible thing to add — would break any :only-child predicate silently.

data-component?undefined

A button's identity is not a caller's to rename — see buttonProps. Typed away so a call site renaming it fails to compile rather than passing an attribute that is silently discarded.

amount?string

What the shopper pays, formatted. The button shows it.

onPlaceOrder?(() => unknown)

Saves the last section and places the order. Return false, or throw, to stay: the section shows why. Anything else forgets the stored confirmations.

type?"button" | "submit"

submit makes it the submit button of the form around it, whose handler places the order and calls the shell's finish; onPlaceOrder is not called.

pendingLabel?ReactNode

What a screen reader announces while pending, for a button that does something other than order.

terms?ReactNode

Under the button: the terms, a cooling-off period.

error?unknown

The place-order mutation's error, shown under the button in the shopper's words.

missing?readonly { id: string; title: string; onSelect: () => void; }[]

What the current section still lacks, named beside the missing sections, such as a payment method that is no longer offered. onSelect takes the shopper to it.

blockedReason?ReactNode

Why the order cannot be placed whatever the shopper fills in, such as CartMinimumOrderReason below the minimum order value: the button is off and says why.

Any other props go to the root element, like on a plain HTML element.

CheckoutSectionOne section of the checkout.
PropTypeDescription
sectionIdstring

The id of one of the definitions the Checkout was given.

hint?ReactNode

What an upcoming section will ask, beside its title.

summary?ReactNode

The confirmed value, shown while the section is done. Never the draft.

notice?ReactNode

One line above the open body: a failed payment, or why the section reopened.

headingLevel?HeadingLevel
children?ReactNode

The open body: the section's fields and its one primary button.

Any other props go to the root element, like on a plain HTML element.

CheckoutSectionContinueThe section's one primary button, at the end of its body.
PropTypeDescription
isPending?boolean

The action is running. The button keeps its label, so its width and accessible name hold, shows a spinner and is aria-busy. It stays focusable rather than disabled, so focus is not dropped mid-save; clicks and the form submit it would trigger are ignored. Not supported with asChild. The save is running, for a submit button whose form owns the save.

tone?ButtonTone
isIconOnly?boolean

The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that.

More

Not inferred from the children. A kebab menu is as often a glyph or an SVG as an <Icon>, and a visually-hidden label — the accessible thing to add — would break any :only-child predicate silently.

data-component?undefined

A button's identity is not a caller's to rename — see buttonProps. Typed away so a call site renaming it fails to compile rather than passing an attribute that is silently discarded.

onContinue?(() => unknown)

Validates and saves the section. Return false, or throw, to keep it open: the section shows why. Anything else confirms it and opens the next one.

type?"button" | "submit"

submit makes it the submit button of the form around it. The form's handler saves and confirms through useCheckoutSectionConfirm; onContinue is not called.

Any other props go to the root element, like on a plain HTML element.

CheckoutStepperThe steps of a multi-step checkout, numbered, with the open one marked current and each confirmed one a button back to it.

No props of its own.

CheckoutSummaryLinesThe cart's lines, compact and read-only: image, name, quantity and line total in the summary's VAT basis.
PropTypeDescription
linesreadonly CartLineItem[]

The cart's lines, cart.lineItems.

vatDisplay?VatDisplay

The basis of the line totals; pass the summary's. Defaults to gross.

Any other props go to the root element, like on a plain HTML element.

CheckoutSummaryToggleThe order summary on a phone: a bar at the top of the checkout, "Show summary · €x", that opens the summary in place.
PropTypeDescription
totalstring

What the shopper pays, formatted, shown on the bar while the summary is closed.

childrenReactNode

The summary itself, usually CartSummary.

defaultOpen?boolean

Any other props go to the root element, like on a plain HTML element.

CheckoutTermsNotice"By placing your order, you agree to our terms and conditions", linking the terms page.
PropTypeDescription
termsPathstring | null | undefined

The terms page's path, such as sitePagePath(settings, "termsPage").

renderLink?((props: { href: string; children: ReactNode; }) => ReactNode)

The link with the app's router. A plain anchor by default.

Any other props go to the root element, like on a plain HTML element.

CheckoutVatOutcomeWhich VAT rule the cart follows and why, such as "VAT reverse-charged · Delivered to another EU country for a business with a valid VAT number." An export adds its delivery terms and the company's EORI number, where set.
PropTypeDescription
outcomeVatOutcome | null | undefined

The cart's vatOutcome; renders nothing while it is null.

companyProfileHref?string

The company profile, where an admin adds a VAT number; links the hint that asks for one.

Any other props go to the root element, like on a plain HTML element.

ExpressCheckoutWallet buttons, above the first checkout field or in the cart: each wallet the cart offers, through renderButton, and nothing when it offers none.
PropTypeDescription
cartCart
renderButton(method: PaymentMethod, handlers: ExpressCheckoutHandlers) => ReactNode

One wallet's button, from the payment provider's integration.

onPlaceOrder(cart: Cart) => unknown

Places and pays the saved cart. Return false, or throw, to stay.

device?ClientDevice
isShippingMethodOffered?((method: ShippingMethod) => boolean)

Leave out a method that needs the full checkout, such as a slot or pickup method.

maxShippingOptions?number

How many shipping options the sheet shows. Defaults to 3.

returnUrls?CheckoutReturnUrls

Where the provider sends the shopper back; needed by a wallet that pays before the order.

onPaid?((outcome: { kind: "paid"; order: { id: string; orderNumber: string; } | null; pending: boolean; }) => unknown)

A wallet that pays before the order finished: route to the order, or to a confirming page.

Any other props go to the root element, like on a plain HTML element.

Loading and empty states

Render these while the data loads or when there is nothing to show, so the page does not jump when it arrives.

LoadingCheckoutSkeletonThe checkout's shape while the cart loads: the first section open, the rest collapsed, and the summary.

Examples

LoadingSkeleton and real checkout adjacent, so a placeholder that drifts from the sections and summary it stands in for shows here.
// One placeholder per section, so the page does not shift when the cart lands.
if (cart.isPending) return <CheckoutSkeleton sections={sections.length} />;
return <Checkout cart={cart.data} sections={sections}>…</Checkout>;
Open in Storybook ↗
Draft SurvivesA draft outlives its section closing, and the summary keeps the confirmed value.
<Checkout cart={cart} sections={sections}>…</Checkout>;
Open in Storybook ↗
Return From Failed PaymentBack from a failed payment: the sections above are confirmed from storage, and Payment opens with the site's notice.
<Checkout cart={cart} sections={sections}>…</Checkout>;
Open in Storybook ↗
Delivery InvalidatedA stored delivery confirmation for another country is dropped, and says why.
<Checkout cart={cart} sections={sections}>…</Checkout>;
Open in Storybook ↗
Multi StepThe same sections one at a time: a stepper above, the confirmed ones as a recap with "Change", and nothing of what is still to come.
<Checkout cart={cart} sections={sections} layout="multi"><CheckoutMain><CheckoutStepper />…</CheckoutMain></Checkout>;
Open in Storybook ↗
Below MinimumBelow the business unit's minimum order value: the button is off whatever is filled in, and says why.
const minimum = useCartMinimumOrder();
<CheckoutPlaceOrder
amount={amount}
onPlaceOrder={placeOrder}
blockedReason={minimum.isBelowMinimum ? <CartMinimumOrderReason minimumOrder={minimum.data} /> : undefined}
/>;
Open in Storybook ↗
Refused Below MinimumThe server refused the order as below the minimum: the shopper reads why, and what to do.
<CheckoutPlaceOrder amount={amount} onPlaceOrder={placeOrder} error={place.error} />;
Open in Storybook ↗