# Checkout steps

> A React multi-step checkout: address and invoice, delivery with lockers, payment and a review that places the order, beside a summary.

- Type: React component (Commerce)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add checkout-steps`
- Docs: https://wingo-ui.com/components/checkout-steps

## Interaction

Four steps under a stepper: Delivery details (email, a saved address card or a new address with county select, city combobox, street and postal code, then an Individual / Company switch that opens the company fields), Shipping (priced courier, easybox and pickup cards; an easybox opens a searchable list of pickup points), Payment (card, cash on delivery with its fee, bank transfer with the IBAN to copy, instalments that unlock above a total) and Review (every choice with an Edit link, notes, the terms box and Place order with the total). Continue checks the step: the fields that are wrong turn red with a message, the button shakes and focus jumps to the first one; each message clears as you fix it. A saved address missing a field shows Complete the address and opens the form. Changing the county clears the city. Choices that change the price (a coupon, a method that becomes free past the threshold, a payment fee) update the cards and the summary in place, and the new total is read out. Place order spins while the order is sent, turns into a check with a small bounce and locks the review, then the success content fades in and takes focus; a refusal shakes the button and shows the reason above it. The summary is a sticky column when the checkout is wide and a bar at the top when it is narrow (Show summary with the total) that unfolds the lines, the promo code and the totals. On a phone each step is a full-height page: the step count and title on top, the total, Back and Continue at the bottom above the home indicator, hidden while the keyboard is up. Keyboard: Tab through the fields, Enter moves to the next field and continues from the last one, arrows move between radio cards and pickup points, Space picks, mod+Enter in the notes continues; completed steps in the stepper are buttons. Under reduced motion steps and panels fade instead of sliding.

## Installation

```bash
npx wingo-ui@latest add checkout-steps
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/checkout-steps` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/checkout-steps.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Cart drawer](https://wingo-ui.com/components/cart-drawer) ([Markdown](https://wingo-ui.com/components/cart-drawer.md))
- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Combobox](https://wingo-ui.com/components/combobox) ([Markdown](https://wingo-ui.com/components/combobox.md))
- [Coupon input](https://wingo-ui.com/components/coupon-input) ([Markdown](https://wingo-ui.com/components/coupon-input.md))
- [Form Wizard](https://wingo-ui.com/components/form-wizard) ([Markdown](https://wingo-ui.com/components/form-wizard.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Phone Input](https://wingo-ui.com/components/phone-input) ([Markdown](https://wingo-ui.com/components/phone-input.md))
- [Price](https://wingo-ui.com/components/price) ([Markdown](https://wingo-ui.com/components/price.md))
- [Radio Group](https://wingo-ui.com/components/radio-group) ([Markdown](https://wingo-ui.com/components/radio-group.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useKeyboardInset](https://wingo-ui.com/components/use-keyboard-inset) ([Markdown](https://wingo-ui.com/components/use-keyboard-inset.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { CheckoutSteps, type DeliveryMethod, type PaymentMethod } from "@/components/ui/checkout-steps"
import type { CartItem } from "@/components/ui/cart-drawer"
import { RO_VAT_RATE } from "@/lib/commerce"
import { cityOptions, countyOptions } from "@/lib/ro-locations"

const items: CartItem[] = [
  { id: "case", title: "iPhone 14 case, silicone, Midnight", price: 79.9, quantity: 2, image: "/demo/phone.jpg" },
  { id: "stand", title: "Aluminum laptop stand", price: 149, quantity: 1, image: "/demo/laptop.jpg" },
]

const delivery: DeliveryMethod[] = [
  { id: "fan", label: "Fan Courier", price: 19.99, eta: "1-2 business days", type: "courier" },
  { id: "pickup", label: "Pick up in Cluj-Napoca", price: 0, type: "pickup" },
]

const payment: PaymentMethod[] = [
  { id: "card", label: "Card" },
  { id: "cash", label: "Cash on delivery", fee: 4.99 },
]

export function Checkout() {
  return (
    <CheckoutSteps
      items={items}
      regions={countyOptions()}
      getCities={(county) => cityOptions(county)}
      deliveryMethods={delivery}
      paymentMethods={payment}
      freeShippingThreshold={250}
      taxRate={RO_VAT_RATE}
      tone="brand"
      onPlaceOrder={(value, totals) =>
        fetch("/api/orders", { method: "POST", body: JSON.stringify({ value, total: totals.total }) })
      }
      successContent={<p>Order #1042 is placed.</p>}
      labels={{
        review: "Confirm",
        placeOrder: (total) => `Pay ${total}`,
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `CartItem[]` |  | The cart lines (CartItem from cart-drawer): id, title, price, quantity, compareAtPrice, taxRate, image, variant. Unavailable lines and quantity 0 are left out; an empty list shows the empty state. |
| `value?` | `CheckoutValue` |  | Controlled checkout data: email, addressId, address (CheckoutAddress), invoice (CheckoutInvoice), deliveryMethod, lockerId, paymentMethod, notes, acceptTerms, coupons (AppliedCoupon[]). Pair it with onValueChange. |
| `defaultValue?` | `CheckoutValue` | `"{}"` | The starting data while uncontrolled (a returning buyer's email, a remembered method). |
| `onValueChange?` | `(value: CheckoutValue) => void` |  | Called with the whole value on every change. |
| `defaultStep?` | `"address" \| "delivery" \| "payment" \| "review"` | `"address"` | The step it opens on while uncontrolled; the steps before it count as done. |
| `step?` | `CheckoutStepId` |  | The controlled step; pair it with onStepChange. |
| `onStepChange?` | `(step: CheckoutStepId) => void` |  | Called when the step changes (Continue, Back, Edit links, the stepper). |
| `steps?` | `CheckoutStepId[]` | `"[\"address\", \"delivery\", \"payment\", \"review\"]"` | The steps in order, e.g. ["payment", "review"] for a digital product. A step whose data is missing (delivery without deliveryMethods) is skipped with a warning in development. |
| `savedAddresses?` | `CheckoutAddress[]` | `"[]"` | Address cards to pick from, plus New address. The first one is chosen at the start; one missing a required field shows Complete the address and opens the form. |
| `regions?` | `{ value: string; label: string; description?: string }[]` |  | The County select (countyOptions() from ro-locations); without it the county is a text field. |
| `getCities?` | `(region: string) => (CheckoutCity[] \| Promise<CheckoutCity[]>)` |  | The City combobox for the chosen county ({ value, label, description, keywords }[]), sync or async (cityOptions(county) from ro-locations); without it a text field. Typed names outside the list are kept. |
| `deliveryMethods?` | `DeliveryMethod[]` |  | Required when the delivery step is used: { id, label, description, price, eta, type: courier \| locker \| pickup, icon, logo, freeFrom, disabled }. Prices turn to Free in place once the threshold or a coupon makes them free. |
| `lockers?` | `CheckoutLocker[] or (methodId: string) => Promise<CheckoutLocker[]>` |  | The pickup points of a locker method ({ id, label, address, distance, hours }), or a loader called per method (skeleton rows while it runs). A locker method then needs a chosen point. |
| `paymentMethods?` | `PaymentMethod[]` |  | Required when the payment step is used: { id, label, description, icon, logos, fee, minTotal, disabled, details }. fee joins the summary and the total; below minTotal the card is disabled with Available from; details (a card form slot, the IBAN) unfold under the cards while chosen. |
| `invoice?` | `boolean` | `true` | Shows the Individual / Company switch and the company fields (name, CUI checked with the ANAF key, trade register no.). |
| `requireTerms?` | `boolean` | `true` | Shows the terms checkbox in the review step and requires it before the order is placed. |
| `termsLabel?` | `ReactNode` | `"labels.terms"` | The terms checkbox label, with links to the terms and privacy pages. |
| `showCoupon?` | `boolean` | `true` | The promo code field in the summary. |
| `couponProps?` | `Partial<CouponInputProps>` |  | Passed to the CouponInput: the coupons catalogue, onApply for a server check, labels. |
| `freeShippingThreshold?` | `number` | `0` | Courier and locker delivery is free from this much (items after coupons); a method's freeFrom wins. 0: no offer. |
| `taxRate?` | `number` | `0` | The VAT rate as a fraction (0.21); 0 hides the VAT row. Lines can carry their own rate. |
| `pricesIncludeTax?` | `boolean` | `true` | Prices include VAT (Romanian B2C); false adds VAT on top as its own row. |
| `summary?` | `"aside" \| "collapsible" \| "none"` | `"aside"` | aside: a sticky summary column once the checkout itself is 896px wide (a container query), a Show summary bar when narrower; collapsible: the bar at every width; none: no summary. |
| `stepper?` | `"horizontal" \| "progress"` | `"horizontal"` | The form-wizard header from 768px: numbered steps, or a progress bar with the step title. Phones always show the compact progress. |
| `fullScreenOnMobile?` | `boolean` | `true` | Below 768px each step fills the container: the summary bar and step count on top, a sticky footer with the total and Continue above the home indicator. |
| `onPlaceOrder?` | `(value: CheckoutValue, totals: CartTotals) => (void \| Promise<unknown>)` |  | Places the order after the review step validates; totals.total includes the payment fee. A promise shows the button loading; a resolve turns it into a check, then successContent; a rejection shows an alert with its message in the review step. |
| `successContent?` | `ReactNode` |  | Replaces the wizard once the order is placed (order number, next steps). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Continue, Place order and the stepper's active marker. |
| `color?` | `string` | `""` | Any CSS color for the same parts; replaces the tone. |
| `size?` | `"sm" \| "md"` | `"md"` | Fields, cards, the stepper and the buttons: sm for a narrow column, md for the page. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | The corners of Back, Continue and Place order. |
| `country?` | `string` | `"RO"` | RO checks the postal code (6 digits) and the CUI control key and starts the phone at +40; other codes relax those checks. |
| `currency?` | `string` | `"RON"` | ISO 4217 code for every amount. |
| `locale?` | `string` | `""` | Locale for prices and numbers; empty uses the UIProvider's, then the browser's. |
| `labels?` | `Partial<CheckoutStepsLabels>` |  | Every string, English by default (DEFAULT_CHECKOUT_STEPS_LABELS): the step titles address "Delivery details", delivery "Shipping", payment "Payment", review "Review"; continue, back, placeOrder(total) "Place order · RON 1,284.90", placed, stepOf(i, n), edit, newAddress, incompleteAddress, contact, the field labels (name, phone, email, county, city, street, details, postalCode, companyName, cui, regCom) and placeholders (emailPlaceholder, countyPlaceholder, cityPlaceholder, cuiPlaceholder, regComPlaceholder), invoiceTitle, person, company, locker, lockerSearch, free, fee(amount), minTotal(amount), paymentFee(method), notes, terms, summary, showSummary(total), hideSummary, the summary rows (subtotal, shipping, total...) and the errors (required, invalidEmail, invalidPhone, invalidCui, invalidPostalCode, acceptTerms, chooseDelivery, chooseLocker, choosePayment). |
| `classNames?` | `Partial<Record<"layout" \| "wizard" \| "summary" \| "summaryBar" \| "address" \| "delivery" \| "payment" \| "review" \| "placeOrder", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### isValidCui (function)

The Romanian tax ID check: an optional "RO" prefix, 2 to 10 digits, and the ANAF control digit (key 753217532). isValidCui("RO12345674") is true, "RO12345678" is false. Signature: (cui: string) => boolean.

### validateCheckoutStep (function)

The same checks the wizard runs, for a server or a custom flow: field errors keyed "email", "address.phone", "invoice.cui", "deliveryMethod", "lockerId", "paymentMethod", "acceptTerms". Options: country, invoice, requireTerms, savedAddresses, deliveryMethods, lockers, paymentMethods, total, labels. Signature: (step: CheckoutStepId, value: CheckoutValue, options?: CheckoutValidationOptions) => Record<string, string>.

### DEFAULT_CHECKOUT_STEPS_LABELS (constant)

The English strings, to spread under a translation. Type: CheckoutStepsLabels.

### CheckoutValue (type)

The checkout state: email, addressId or address, invoice, deliveryMethod, lockerId, paymentMethod, notes, acceptTerms and coupons.

### CheckoutAddress (type)

A saved or new address: id, label ("Home"), name, phone, email, county, city, street, details and postalCode.

### DeliveryMethod (type)

A shipping card: id, label, description, price, eta, type ("courier" | "locker" | "pickup"), icon or logo, freeFrom and disabled.

### CheckoutLocker (type)

A pickup point in the locker list: id, label, address, distance and hours.

### PaymentMethod (type)

A payment card: id, label, description, icon, logos, fee (added while chosen), minTotal (available from this total), disabled and details shown while chosen.

### CheckoutStepId (type)

"address" | "delivery" | "payment" | "review", for steps and step.

## Examples

- **Company invoice**: A guest orders for Carpathia Trading SRL: Continue flags the tax ID RO12345678; RO12345674 passes the Romanian CUI check.
- **Easybox**: A locker method opens a searchable list of pickup points, loaded from the courier; free from 250 RON.
- **Payment declined**: onPlaceOrder rejects: the button stops spinning and the bank's message shows in the review step.
- **Progress stepper, brand**: stepper="progress" and tone="brand" for Continue and Place order, VAT 21% included.
- **Digital product**: steps={["payment", "review"]}: a subscription needs no address or delivery.
- **Another country, EUR**: country="DE" relaxes the postal code and tax ID checks: en-US and EUR, size="sm", no invoice switch, the summary as a bar.
- **Phone**: Each step is a full-height page with the summary bar on top and the total next to Continue.

## See also

- [Checkout Page](https://wingo-ui.com/components/checkout-page) ([Markdown](https://wingo-ui.com/components/checkout-page.md))
- [Form Wizard](https://wingo-ui.com/components/form-wizard) ([Markdown](https://wingo-ui.com/components/form-wizard.md))
- [Cart drawer](https://wingo-ui.com/components/cart-drawer) ([Markdown](https://wingo-ui.com/components/cart-drawer.md))
- [Coupon input](https://wingo-ui.com/components/coupon-input) ([Markdown](https://wingo-ui.com/components/coupon-input.md))
- [Radio Group](https://wingo-ui.com/components/radio-group) ([Markdown](https://wingo-ui.com/components/radio-group.md))
- [Phone Input](https://wingo-ui.com/components/phone-input) ([Markdown](https://wingo-ui.com/components/phone-input.md))

---

Canonical: https://wingo-ui.com/components/checkout-steps
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
