All Taste components
Animated screens
Payment success
A payment confirmation where the circle draws, the tick strokes in, the amount counts up and the receipt slides up.
ring draw · tick · counting amount · receipt sheet
Payment successful
to Hearth & Grain Bakery
16 Oct 2026, 9:41 AM
UPI IDhearthgrain@okbank
FromBluefin Bank ••4821
Transaction IDT2610160941384
Paid forOrder 2207 · 3 items
// Payment success: the ring draws, the disc fills, the tick strokes in, the amount counts up
// and the receipt slides up. Plays once on mount. Mobile-first on the Taste preset
// (animate-draw, animate-pop, animate-rise and animate-slide-up come with it).
"use client";
import { useEffect, useState } from "react";
import { CircleHelp, Share, X } from "lucide-react";
interface Receipt {
amount: number; // in rupees
payee: string;
upiId: string;
from: string;
txnId: string;
note: string;
paidAt: string;
}
/** Counts from 0 to value once, easing out; shows the value straight away for reduced motion. */
function useCountUp(value: number, delay = 400, duration = 1200) {
const [n, setN] = useState(value);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let raf = 0;
const t0 = performance.now() + delay;
const tick = (t: number) => {
const p = Math.min(1, Math.max(0, (t - t0) / duration));
setN(value * (1 - Math.pow(1 - p, 4)));
if (p < 1) raf = requestAnimationFrame(tick);
};
setN(0);
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [value, delay, duration]);
return n;
}
const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", minimumFractionDigits: 2 });
export default function PaymentSuccess({ receipt, onDone }: { receipt: Receipt; onDone: () => void }) {
const shown = useCountUp(receipt.amount);
const rows: [string, string][] = [
["UPI ID", receipt.upiId],
["From", receipt.from],
["Transaction ID", receipt.txnId],
["Paid for", receipt.note],
];
return (
<main className="mx-auto flex min-h-dvh max-w-md flex-col bg-bg font-sans text-fg">
<header className="flex h-12 items-center justify-between px-1.5">
<button type="button" aria-label="Close" onClick={onDone} className="grid size-10 place-items-center rounded-full hover:bg-surface-2"><X className="size-5" /></button>
<a href="/help" aria-label="Help" className="grid size-10 place-items-center rounded-full hover:bg-surface-2"><CircleHelp className="size-5" /></a>
</header>
<section role="status" className="grid flex-1 content-start justify-items-center gap-0.5 px-4 pt-2 text-center">
<svg viewBox="0 0 104 104" className="mb-3.5 size-[104px] overflow-visible" aria-hidden>
<circle cx="52" cy="52" r="46" className="fill-success-soft animate-fade motion-reduce:animate-none" />
<circle cx="52" cy="52" r="46" pathLength={1} transform="rotate(-90 52 52)" fill="none" strokeWidth="4" strokeLinecap="round" strokeDasharray="1" className="stroke-success animate-draw motion-reduce:animate-none" />
<circle cx="52" cy="52" r="37" className="origin-center fill-success [transform-box:fill-box] animate-pop [animation-delay:.9s] motion-reduce:animate-none" />
<path d="M36 53l11 11 22-24" pathLength={1} fill="none" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" strokeDasharray="1" className="stroke-bg animate-draw [animation-delay:1.15s] [animation-duration:.6s] motion-reduce:animate-none" />
</svg>
<h1 className="animate-rise text-[17px] font-semibold [animation-delay:1.2s] motion-reduce:animate-none">Payment successful</h1>
<p className="my-0.5 text-4xl font-semibold leading-[44px] tracking-[-.03em] tabular-nums" aria-label={inr.format(receipt.amount)}>
{inr.format(shown)}
</p>
<p className="animate-rise text-[13.5px] text-fg-muted [animation-delay:1.3s] motion-reduce:animate-none">to <b className="font-medium text-fg">{receipt.payee}</b></p>
<p className="animate-rise text-xs text-fg-muted [animation-delay:1.4s] motion-reduce:animate-none">{receipt.paidAt}</p>
</section>
<section aria-label="Receipt" className="animate-slide-up rounded-t-[calc(var(--tst-radius-xl)+4px)] bg-surface px-4 pb-7 pt-4 ring-1 ring-border [animation-delay:1.6s] motion-reduce:animate-none">
<div aria-hidden className="mx-auto -mt-1.5 mb-2.5 h-1 w-9 rounded-full bg-border-strong" />
<dl className="divide-y divide-dashed divide-border-strong text-[12.5px]">
{rows.map(([k, val]) => (
<div key={k} className="flex justify-between gap-3 py-[7px]">
<dt className="text-fg-muted">{k}</dt>
<dd className="min-w-0 text-right font-medium tabular-nums">{val}</dd>
</div>
))}
</dl>
<div className="mt-3 grid grid-cols-2 gap-2">
<button type="button" className="inline-flex h-11 items-center justify-center gap-1.5 rounded border border-border-strong bg-bg text-sm font-semibold hover:bg-surface"><Share className="size-4" />Share</button>
<button type="button" onClick={onDone} className="h-11 rounded bg-primary text-sm font-semibold text-primary-fg hover:bg-primary-hover">Done</button>
</div>
</section>
</main>
);
}
// <PaymentSuccess onDone={close} receipt={{ amount: 2450, payee: "Hearth & Grain Bakery", upiId: "hearthgrain@okbank",
// from: "Bluefin Bank ••4821", txnId: "T2610160941384", note: "Order 2207 · 3 items", paidAt: "16 Oct 2026, 9:41 AM" }} />
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.