Live Checkout
A SaaS upgrade checkout whose summary recomputes as you edit, then glides to the centre and unfolds into a receipt.
Overview
The paid signup or upgrade step of a SaaS app, on one page: plan, seats, monthly or yearly billing, add-ons, a coupon, the billing country and the card. Every change recomputes the summary (seat lines, add-ons, the yearly discount, the coupon, tax for the chosen country) and each amount rolls digit by digit to its new value.
Details carry the motion: the selected-plan ring and the billing pill travel between options on a spring, add-on and discount lines open and close their height, the coupon field turns into a removable chip once your server accepts it, and declined payments shake in with the reason. On success the form steps aside and the summary glides to the centre and unfolds into a receipt with a downloadable copy.
Plans, add-ons and countries (tax rate, tax label, postal-code format) are props, coupons go through validateCoupon and charging through onPay, so it plugs into Stripe, Paddle, HandyPay or your own backend. computeTotals is exported so your server can use the same maths.
When to use
- Upgrading from a free plan to a paid team plan.
- Paid signup where seats and add-ons change the price.
- Self-serve checkout that needs coupons and tax by country.
When not to use
- A physical goods cart with shipping: use Tender Checkout.
- Only showing totals: use Seat Pricing.
Installation
pnpm dlx shadcn@latest add https://sundaeui.dev/r/live-checkout.jsonnpx shadcn@latest add https://sundaeui.dev/r/live-checkout.jsonyarn dlx shadcn@latest add https://sundaeui.dev/r/live-checkout.jsonbunx --bun shadcn@latest add https://sundaeui.dev/r/live-checkout.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view. Pro assets need a personal token. See authenticated installs.
- No dependencies beyond React 18 or later.
- Copy live-checkout.tsx and live-checkout.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { LiveCheckout } from '@/components/sundae/live-checkout'
export function UpgradePage() {
return (
<LiveCheckout
defaultPlan="team"
defaultSeats={12}
validateCoupon={(code) => fetch(`/api/coupons/${code}`).then((r) => r.json())}
onPay={async (order) => {
const res = await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(order) })
if (!res.ok) throw new Error('Your card was declined. Try another card.')
}}
/>
)
}Props
| Prop | Type | Default |
|---|---|---|
plansid, name, seatCents (per seat per month) and a blurb. | Plan[] | sample plans |
addonsid, name, monthlyCents and an optional blurb. | Addon[] | sample add-ons |
countriescode, name, taxRate (0.2 = 20%), taxLabel, optional postal RegExp and postalHint. | Country[] | sample countries |
defaultPlan / defaultSeats / defaultBilling / defaultAddons / defaultCountryStarting selections. Seats are 1–500. | string / number / Billing / string[] / string | — |
yearlyDiscountYearly discount, 0–1, shown on the billing toggle. | number | 0.2 |
currencyISO code used to format every amount. | string | 'USD' |
validateCouponReturn { ok: true, code, percent } or { ok: false, message }. | (code) => CouponResult | Promise<CouponResult> | sample: LAUNCH20, TEAM10 |
onPayCharge the order; throw an Error to show its message. Without it the demo simulates payment and declines 4000 0000 0000 0002. | (order: CheckoutOrder) => void | Promise<void> | — |
onDoneCalled from the receipt’s Done button. Default resets the form. | () => void | — |
themeauto follows a .dark ancestor. | 'auto' | 'light' | 'dark' | 'auto' |
Accessibility
- Plans and the billing period are radio groups (arrow keys move between plans); add-ons are real checkboxes.
- Errors are tied to their inputs with aria-describedby and aria-invalid; a failed submit focuses the first invalid field; payment errors use role="alert".
- Rolling amounts keep the full value in visually hidden text, so screen readers never hear single digits.
- On success focus moves to the receipt heading. Reduced motion keeps every state and drops the travel, rolls and unfold.
Customization
- Custom properties on .sd-lc: --lc-accent (pay button), --lc-ring (selected plan), --lc-good, --lc-err, --lc-panel, --lc-field.
- Two columns with a sticky summary from 760px of container width; below that the summary sits on top as a collapsible total. Plans stack under 520px.
- computeTotals, luhn and RollingAmount are exported.
Keyboard
- Arrow keys
- Move between plans
- Enter (coupon field)
- Apply the coupon
- Escape (coupon field)
- Close the coupon field
- Enter
- Submit the payment
FAQ
- Does it collect real card details?
- It validates the card fields for layout and feedback. In production, swap them for your payment provider’s hosted fields and pass the token in onPay; the rest of the layout stays.
- Is Live Checkout free?
- Live Checkout is part of Sundae Pro. The live preview is open to everyone; the source installs with a Pro account.
- How do I install Live Checkout?
- Run npx shadcn@latest add https://sundaeui.dev/r/live-checkout.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Live Checkout support keyboard and screen readers?
- Plans and the billing period are radio groups (arrow keys move between plans); add-ons are real checkboxes. Errors are tied to their inputs with aria-describedby and aria-invalid; a failed submit focuses the first invalid field; payment errors use role="alert".
- Does Live Checkout respect reduced motion?
- On success focus moves to the receipt heading. Reduced motion keeps every state and drops the travel, rolls and unfold.
More like this
All checkout →Trial Countdown
Checkout · Refined- Pro
Seat Checkout
Checkout · Understated - Pro
Billing Toggle
Checkout · Refined Coupon Field
Checkout · Refined- Pro
Seat Pricing
Checkout · Refined - Pro
Payment Methods
Checkout · Refined