All Taste components
Animated screens
Checkout stepper
A four-step checkout where the progress line moves from cart to address to payment to done.
4 steps · springing progress line · cards swap · done check
- 1Cart
- 2Address
- 3Pay
- 4Done
Linen throw cushion
₹899Sage · 45 × 45 cm
Ceramic table lamp
₹1,299Off-white · warm bulb
- Subtotal
- ₹2,198
- Festive offer
- −₹352
- Delivery
- Free
Deliver to
Home
Flat 4B, Lakeview Residency, 12th Main, Indiranagar, Bengaluru 560038
Work
Level 3, Prestige Atrium, Residency Rd, Bengaluru 560025
Arrives Sun, 18 Oct · Free delivery
Pay with
UPI
priya.r@okbank
Card
Visa ending 4421
Cash on delivery
₹20 handling fee
Order placed
#SX-20481 · Arrives Sun, 18 Oct
Paid ₹1,846 with UPI
Total
₹1,846/* globals.css
@keyframes panel-in { from { opacity: 0; transform: translateX(28px); } }
@keyframes pop { from { opacity: 0; transform: scale(.5); } }
*/
"use client";
import { useState } from "react";
import { ArrowLeft, Banknote, Briefcase, CalendarDays, Check, CreditCard, House, Smartphone } from "lucide-react";
import { cn } from "@/lib/utils";
const steps = ["Cart", "Address", "Pay", "Done"];
const cta = ["Continue to address", "Continue to payment", "Pay ₹1,846", "Track order"];
const cart = [
{ name: "Linen throw cushion", meta: "Sage · 45 × 45 cm", price: "₹899" },
{ name: "Ceramic table lamp", meta: "Off-white · warm bulb", price: "₹1,299" },
];
function Option({ on, icon: Icon, title, note, onClick }: { on: boolean; icon: typeof House; title: string; note: string; onClick: () => void }) {
return (
<button type="button" role="radio" aria-checked={on} onClick={onClick}
className={cn("mt-2 flex w-full gap-3 rounded-lg border p-3 text-left transition-colors", on ? "border-primary bg-primary-soft/50" : "border-border")}>
<span className={cn("mt-0.5 size-[18px] shrink-0 rounded-full border-2", on ? "border-[5px] border-primary" : "border-border-strong")} />
<span className="min-w-0"><span className="flex items-center gap-1.5 text-sm font-semibold"><Icon className="size-3.5" />{title}</span><span className="text-xs text-fg-muted">{note}</span></span>
</button>
);
}
export default function CheckoutScreen() {
const [step, setStep] = useState(0);
const [address, setAddress] = useState("home");
const [pay, setPay] = useState("upi");
return (
<div 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 px-2">
<button type="button" aria-label="Back" disabled={step === 0 || step === 3} onClick={() => setStep(step - 1)} className="grid size-11 place-items-center rounded-full disabled:opacity-30"><ArrowLeft className="size-[22px]" /></button>
<h1 className="flex-1 text-center text-[15px] font-semibold">Checkout</h1>
<span className="w-11" />
</header>
<ol aria-label="Checkout progress" className="relative grid grid-cols-4 px-4 pb-3.5">
<span aria-hidden className="absolute inset-x-[calc(12.5%+4px)] top-[13px] h-[3px] rounded-full bg-border">
<span className="block h-full origin-left rounded-full bg-primary transition-transform duration-700 ease-spring motion-reduce:transition-none" style={{ transform: "scaleX(" + step / 3 + ")" }} />
</span>
{steps.map((s, i) => {
const done = i < step || step === 3;
return (
<li key={s} aria-current={i === step ? "step" : undefined} className={cn("relative grid justify-items-center gap-1 text-[11px] font-medium transition-colors duration-300", i <= step ? "text-fg" : "text-fg-subtle")}>
<span className={cn("grid size-7 place-items-center rounded-full border-2 text-xs font-semibold transition-colors duration-300",
done ? "border-primary bg-primary text-primary-fg" : i === step ? "border-primary bg-primary-soft text-primary-soft-fg" : "border-border-strong bg-bg text-fg-muted")}>
{done ? <Check className="size-3.5 animate-[pop_.35s_cubic-bezier(.34,1.56,.64,1)_both]" /> : i + 1}
</span>
{s}
</li>
);
})}
</ol>
{/* key remounts the panel so each step slides in. */}
<main key={step} className="flex-1 px-4 animate-[panel-in_.45s_cubic-bezier(.2,.7,.1,1)_both] motion-reduce:animate-none">
{step === 0 && (
<>
{cart.map((c) => (
<div key={c.name} className="flex items-center gap-3 border-b border-border py-2.5">
<span aria-hidden className="size-12 shrink-0 rounded bg-surface-2" />
<div className="min-w-0 flex-1"><p className="text-sm font-semibold">{c.name}</p><p className="text-xs text-fg-muted">{c.meta}</p></div>
<span className="text-sm font-semibold tabular-nums">{c.price}</span>
</div>
))}
<dl className="mt-3 grid gap-2 text-[13.5px] tabular-nums">
<div className="flex justify-between"><dt className="text-fg-muted">Subtotal</dt><dd>₹2,198</dd></div>
<div className="flex justify-between"><dt className="text-fg-muted">Festive offer</dt><dd className="text-success-fg">−₹352</dd></div>
<div className="flex justify-between"><dt className="text-fg-muted">Delivery</dt><dd>Free</dd></div>
</dl>
</>
)}
{step === 1 && (
<div role="radiogroup" aria-label="Deliver to">
<p className="text-xs font-medium text-fg-muted">Deliver to</p>
<Option on={address === "home"} onClick={() => setAddress("home")} icon={House} title="Home" note="Flat 4B, Lakeview Residency, 12th Main, Indiranagar, Bengaluru 560038" />
<Option on={address === "work"} onClick={() => setAddress("work")} icon={Briefcase} title="Work" note="Level 3, Prestige Atrium, Residency Rd, Bengaluru 560025" />
<p className="mt-3 flex items-center gap-1.5 text-xs text-success-fg"><CalendarDays className="size-3.5" />Arrives Sun, 18 Oct · Free delivery</p>
</div>
)}
{step === 2 && (
<div role="radiogroup" aria-label="Pay with">
<p className="text-xs font-medium text-fg-muted">Pay with</p>
<Option on={pay === "upi"} onClick={() => setPay("upi")} icon={Smartphone} title="UPI" note="priya.r@okbank" />
<Option on={pay === "card"} onClick={() => setPay("card")} icon={CreditCard} title="Card" note="Visa ending 4421" />
<Option on={pay === "cod"} onClick={() => setPay("cod")} icon={Banknote} title="Cash on delivery" note="₹20 handling fee" />
</div>
)}
{step === 3 && (
<div className="grid justify-items-center pt-6 text-center">
<span className="grid size-16 place-items-center rounded-full bg-success-soft text-success-fg animate-[pop_.5s_cubic-bezier(.34,1.56,.64,1)_.15s_both] motion-reduce:animate-none"><Check className="size-8" /></span>
<h2 className="mt-3 text-base font-semibold">Order placed</h2>
<p className="text-xs tabular-nums text-fg-muted">#SX-20481 · Arrives Sun, 18 Oct</p>
</div>
)}
</main>
<footer className="flex items-center gap-3 border-t border-border px-4 pb-[max(env(safe-area-inset-bottom),16px)] pt-3">
<div><p className="text-xs text-fg-muted">Total</p><p className="font-semibold tabular-nums">₹1,846</p></div>
<button type="button" onClick={() => setStep(Math.min(step + 1, 3))} className="h-12 flex-1 rounded bg-primary text-[15px] font-semibold text-primary-fg hover:bg-primary-hover">{cta[step]}</button>
</footer>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.