Payment Success Sheet
A full bottom-sheet payment flow React component: the sheet slides up, the pay button morphs into a processing ring, the ring morphs into a check with a single pulse, and the order summary unfolds row by row.
The bottom sheet for the last step of checkout. Press pay and the full-width button collapses into a 56px circle with a spinning progress ring; on success the ring collapses, a check draws in a single stroke and the button pulses once, then the title switches to "付款成功" (Payment successful) and summary rows such as order number, payment time and estimated delivery unfold 70ms apart, finally calling onDone. The sheet can be dragged down by its handle to close, and every close path is locked while the payment is processing. Both the open state and the payment stage support controlled and uncontrolled modes.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add payment-success-sheet.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<PaymentSuccessSheet/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/payment-success-sheet.en.jsonOr, once registries are configured in components.json, install it as @webberui/payment-success-sheet.
Usage
import { PaymentSuccessSheet } from "@/components/ui/payment-success-sheet";
<PaymentSuccessSheet
items={[
{ id: "cushion", name: "亞麻抱枕套", qty: 1, price: 680 },
{ id: "tray", name: "手作木托盤", qty: 1, price: 600 },
]}
total={1280}
currency="TWD"
paymentMethod="•••• 4242"
summary={[
{ label: "訂單編號", value: "MB-20261025-0412" },
{ label: "合計", value: "NT$1,280" },
{ label: "預計到貨", value: "10/28(二)" },
]}
onPay={async () => {
// Call your payment API; resolve → success, reject → back to idle
await chargeCard();
}}
onDone={() => console.log("Animation finished, print the receipt next")}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state; managed internally when omitted |
defaultOpen | boolean | false | Initial open state in uncontrolled mode |
onOpenChange | (open: boolean) => void | — | Called when the open state changes |
stage | "idle" | "processing" | "success" | — | Controlled payment stage; the parent advances it |
defaultStage | "idle" | "processing" | "success" | "idle" | Initial stage in uncontrolled mode |
onStageChange | (stage: PaymentSuccessSheetStage) => void | — | Called when the payment stage changes |
items | PaymentSuccessSheetItem[] | — | Cart items |
total | number | — | Amount due |
currency | string | "TWD" | Currency code that picks the amount prefix (TWD → NT$, USD → $, JPY → ¥, EUR → €, HKD → HK$; any other code is shown as-is) |
paymentMethod | ReactNode | — | Payment method row content; the row is hidden when omitted |
summary | PaymentSuccessSheetSummaryRow[] | payment method + total | Summary rows revealed one by one after success |
onPay | () => Promise<void> | void | — | Called when pay is pressed; uncontrolled, resolve → success and reject → idle |
onDone | () => void | — | Called once the success animation and summary have fully played |
stiffness | number | 340 | Stiffness of the sheet's slide-up spring (damping is fixed at 34) |
showToast | boolean | true | Float a "已加入訂單" (Added to orders) toast above the sheet for 2 seconds after success |
showTrigger | boolean | true | Whether to render the built-in "前往結帳" (Checkout) trigger |
contained | boolean | false | Position the sheet inside the nearest positioned ancestor without locking body scroll (for demo frames and previews) |
labels | Partial<PaymentSuccessSheetLabels> | — | UI text overrides |
className | string | — | Merged onto the root element |
PaymentSuccessSheetItem
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id |
name | string | — | Item name |
qty | number | — | Quantity |
price | number | — | Unit price; the row shows unit price × quantity |
thumb | ReactNode | first-character tile | Thumbnail |
PaymentSuccessSheetSummaryRow
| Field | Type | Default | Description |
|---|---|---|---|
label | string | — | Row label |
value | ReactNode | — | Row value |
PaymentSuccessSheetLabels
| Field | Type | Default | Description |
|---|---|---|---|
trigger | string | "前往結帳" (Checkout) | Built-in trigger text |
title | string | "確認付款" (Confirm payment) | Title before payment |
successTitle | string | "付款成功" (Payment successful) | Title after success |
pay | string | "付款" (Pay) | Pay button prefix, followed by the total |
processing | string | "付款處理中" (Processing payment) | Screen reader announcement while processing |
processingButton | string | "處理中…" (Processing…) | Button text under reduced motion |
success | string | "付款成功" (Payment successful) | Screen reader announcement on success |
paymentMethod | string | "付款方式" (Payment method) | Payment method row label |
total | string | "合計" (Total) | Total row label |
close | string | "關閉" (Close) | Accessible name of the close button |
dragHandle | string | "往下拖曳以關閉" (Drag down to close) | Drag handle hint |
toast | string | "已加入訂單" (Added to orders) | Success toast text |
How it works
- Timing: the sheet slides up on a spring (
stiffness340 by default, damping 34) while the backdrop fades in over 200ms; the pay button's width morph uses a spring with bounce 0 and 0.26s duration; the check draws in 400ms and the pulse runs 500ms (scale 1 → 1.12 → 1 with a fading outer ripple). - The success colour comes from the CSS variable
--wb-pss-success(default#10b981) and can be overridden at the theme level. - Uncontrolled, processing lasts at least 0.8 seconds so the ring does not flash when
onPayresolves instantly; controlled, the component only callsonStageChange("processing")and you decide when to switch tosuccess. - In uncontrolled mode, closing the sheet after success resets it to
idle, so the next open is a fresh checkout. - Dragging starts only from the top handle: dragging down more than 100px or faster than 500px/s closes the sheet; otherwise it springs back.
- To print a receipt after a successful payment, chain the receipt-printer component (Pro, same batch) after
onDone; this component does not depend on it. - Money formatting is deterministic (digits are grouped by hand rather than with
Intl.NumberFormat), so server and browser output match and hydration stays consistent.
Accessibility
- The sheet is
role="dialog"witharia-modal="true"andaria-labelledbypointing at the title; on open focus moves to the pay button, Tab / Shift+Tab are trapped inside the sheet, and focus returns to the previously focused element on close - Esc, clicking the backdrop, dragging the handle and the close button all close the sheet; all of them are disabled while the payment is processing
- The pay button sets
aria-busyand is disabled while processing, and keeps a readable name viaaria-labelonce it shrinks into a circle - A visually hidden
role="status"region witharia-live="polite"announces "processing" and then "payment successful" - When the user's system has reduce motion enabled, the sheet fades in over 150ms, the pay button does not morph but switches straight to "處理中…" (Processing…) text, the check appears without a pulse, summary rows show immediately and dragging is disabled
Transaction List
A finance-app transaction list React component: rows grouped by date automatically, rolling amounts coloured by sign, neighbouring rows shifting smoothly when a new transaction is inserted, plus swipe-to-pin and status chips.
Morphing Dialog
Click a card and it scales and morphs seamlessly into a centered dialog with a shared-element animation, then shrinks back to the original card position on close.