Commerce & Checkout

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

340
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
openboolean—Controlled open state; managed internally when omitted
defaultOpenbooleanfalseInitial 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
itemsPaymentSuccessSheetItem[]—Cart items
totalnumber—Amount due
currencystring"TWD"Currency code that picks the amount prefix (TWD → NT$, USD → $, JPY → ¥, EUR → €, HKD → HK$; any other code is shown as-is)
paymentMethodReactNode—Payment method row content; the row is hidden when omitted
summaryPaymentSuccessSheetSummaryRow[]payment method + totalSummary 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
stiffnessnumber340Stiffness of the sheet's slide-up spring (damping is fixed at 34)
showToastbooleantrueFloat a "已加入訂單" (Added to orders) toast above the sheet for 2 seconds after success
showTriggerbooleantrueWhether to render the built-in "前往結帳" (Checkout) trigger
containedbooleanfalsePosition the sheet inside the nearest positioned ancestor without locking body scroll (for demo frames and previews)
labelsPartial<PaymentSuccessSheetLabels>—UI text overrides
classNamestring—Merged onto the root element

PaymentSuccessSheetItem

FieldTypeDefaultDescription
idstring—Unique id
namestring—Item name
qtynumber—Quantity
pricenumber—Unit price; the row shows unit price × quantity
thumbReactNodefirst-character tileThumbnail

PaymentSuccessSheetSummaryRow

FieldTypeDefaultDescription
labelstring—Row label
valueReactNode—Row value

PaymentSuccessSheetLabels

FieldTypeDefaultDescription
triggerstring"前往結帳" (Checkout)Built-in trigger text
titlestring"確認付款" (Confirm payment)Title before payment
successTitlestring"付款成功" (Payment successful)Title after success
paystring"付款" (Pay)Pay button prefix, followed by the total
processingstring"付款處理中" (Processing payment)Screen reader announcement while processing
processingButtonstring"處理中…" (Processing…)Button text under reduced motion
successstring"付款成功" (Payment successful)Screen reader announcement on success
paymentMethodstring"付款方式" (Payment method)Payment method row label
totalstring"合計" (Total)Total row label
closestring"關閉" (Close)Accessible name of the close button
dragHandlestring"往下拖曳以關閉" (Drag down to close)Drag handle hint
toaststring"已加入訂單" (Added to orders)Success toast text

How it works

  • Timing: the sheet slides up on a spring (stiffness 340 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 onPay resolves instantly; controlled, the component only calls onStageChange("processing") and you decide when to switch to success.
  • 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" with aria-modal="true" and aria-labelledby pointing 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-busy and is disabled while processing, and keeps a readable name via aria-label once it shrinks into a circle
  • A visually hidden role="status" region with aria-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