Skip to content
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.