Checkout steps React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React multi-step checkout: address and invoice, delivery with lockers, payment and a review that places the order, beside a summary.
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.
Checkout steps is a Pro React component in the React commerce components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add checkout-steps, together with Alert, Badge, Button, Cart drawer, Checkbox and 18 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add checkout-stepsCheckout steps is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import CheckoutSteps from @/components/ui/checkout-steps.
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
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.
valueCheckoutValueControlled checkout data: email, addressId, address (CheckoutAddress), invoice (CheckoutInvoice), deliveryMethod, lockerId, paymentMethod, notes, acceptTerms, coupons (AppliedCoupon[]). Pair it with onValueChange.
defaultValuedefault"{}"CheckoutValueThe starting data while uncontrolled (a returning buyer's email, a remembered method).
onValueChange(value: CheckoutValue) => voidCalled with the whole value on every change.
defaultStepdefault"address""address" | "delivery" | "payment" | "review"The step it opens on while uncontrolled; the steps before it count as done.
stepCheckoutStepIdThe controlled step; pair it with onStepChange.
onStepChange(step: CheckoutStepId) => voidCalled when the step changes (Continue, Back, Edit links, the stepper).
stepsdefault"["address", "delivery", "payment", "review"]"CheckoutStepId[]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.
savedAddressesdefault"[]"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.
deliveryMethodsDeliveryMethod[]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.
lockersCheckoutLocker[] 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.
paymentMethodsPaymentMethod[]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.
invoicedefaulttruebooleanShows the Individual / Company switch and the company fields (name, CUI checked with the ANAF key, trade register no.).
requireTermsdefaulttruebooleanShows the terms checkbox in the review step and requires it before the order is placed.
termsLabeldefault"labels.terms"ReactNodeThe terms checkbox label, with links to the terms and privacy pages.
showCoupondefaulttruebooleanThe promo code field in the summary.
couponPropsPartial<CouponInputProps>Passed to the CouponInput: the coupons catalogue, onApply for a server check, labels.
freeShippingThresholddefault0numberCourier and locker delivery is free from this much (items after coupons); a method's freeFrom wins. 0: no offer.
taxRatedefault0numberThe VAT rate as a fraction (0.21); 0 hides the VAT row. Lines can carry their own rate.
pricesIncludeTaxdefaulttruebooleanPrices include VAT (Romanian B2C); false adds VAT on top as its own row.
summarydefault"aside""aside" | "collapsible" | "none"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.
stepperdefault"horizontal""horizontal" | "progress"The form-wizard header from 768px: numbered steps, or a progress bar with the step title. Phones always show the compact progress.
fullScreenOnMobiledefaulttruebooleanBelow 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.
successContentReactNodeReplaces the wizard once the order is placed (order number, next steps).
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Continue, Place order and the stepper's active marker.
colordefault""stringAny CSS color for the same parts; replaces the tone.
sizedefault"md""sm" | "md"Fields, cards, the stepper and the buttons: sm for a narrow column, md for the page.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"The corners of Back, Continue and Place order.
countrydefault"RO"stringRO checks the postal code (6 digits) and the CUI control key and starts the phone at +40; other codes relax those checks.
currencydefault"RON"stringISO 4217 code for every amount.
localedefault""stringLocale for prices and numbers; empty uses the UIProvider's, then the browser's.
labelsPartial<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).
classNamesPartial<Record<"layout" | "wizard" | "summary" | "summaryBar" | "address" | "delivery" | "payment" | "review" | "placeOrder", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last.
Parts
isValidCui
functionThe 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
functionThe 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
constantThe English strings, to spread under a translation. Type: CheckoutStepsLabels.
CheckoutValue
typeThe checkout state: email, addressId or address, invoice, deliveryMethod, lockerId, paymentMethod, notes, acceptTerms and coupons.
CheckoutAddress
typeA saved or new address: id, label ("Home"), name, phone, email, county, city, street, details and postalCode.
DeliveryMethod
typeA shipping card: id, label, description, price, eta, type ("courier" | "locker" | "pickup"), icon or logo, freeFrom and disabled.
CheckoutLocker
typeA pickup point in the locker list: id, label, address, distance and hours.
PaymentMethod
typeA 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.
Dependencies
Files
- components/ui/checkout-steps.tsx
FAQ
Is the React Checkout steps component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Checkout steps?
Run npx wingo-ui@latest add checkout-steps. It also adds the Wingo UI items it builds on (Alert, Badge, Button, Cart drawer, Checkbox and 18 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/checkout-steps.
How does Checkout steps behave on phones?
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.
See also
Used in
More React commerce components
Articles using this component
- React Checkout Page: Cart, Steps, Coupons and a Pay Bar
Build it: screens and apps12 min read
- React Stepper Component: States, Variants and Phones
Component guides8 min read