Skip to content
All Taste components

Commerce

Checkout summary

An order summary with line items, a coupon, GST and the total on the pay button.

line items · applied coupon · subtotal · discount · GST 18% · pay button

Order summary

  • Growth plan, yearly
    Up to 4 seats · billed once
    ₹30,000
  • Extra seats
    4 × ₹2,500
    ₹10,000
WELCOME10 · 10% off
Subtotal₹40,000
Discount−₹4,000
GST (18%)₹6,480
Total₹42,480

Secure payment · GST invoice emailed to you

"use client";
import { useState } from "react";
import { Lock, ShieldCheck, Ticket } from "lucide-react";

export interface LineItem { id: string; name: string; detail?: string; qty: number; unitPrice: number; icon?: React.ReactNode }

const inr = (v: number) => "₹" + Math.round(v).toLocaleString("en-IN");

const sample: LineItem[] = [
  { id: "plan", name: "Growth plan, yearly", detail: "Up to 4 seats · billed once", qty: 1, unitPrice: 30000 },
  { id: "seats", name: "Extra seats", qty: 4, unitPrice: 2500 },
];

/** Coupons you accept: code → percent off. Validate on the server too. */
const coupons: Record<string, number> = { WELCOME10: 10 };

export function CheckoutSummary({ items = sample, gstRate = 0.18, onPay }:
  { items?: LineItem[]; gstRate?: number; onPay?: (total: number, coupon?: string) => void }) {
  const [code, setCode] = useState("");
  const [applied, setApplied] = useState<string | undefined>("WELCOME10");
  const [error, setError] = useState<string | null>(null);

  const subtotal = items.reduce((s, i) => s + i.qty * i.unitPrice, 0);
  const discount = applied ? (subtotal * coupons[applied]) / 100 : 0;
  const gst = (subtotal - discount) * gstRate;
  const total = subtotal - discount + gst;

  const apply = () => {
    const c = code.trim().toUpperCase();
    if (coupons[c]) { setApplied(c); setError(null); setCode(""); }
    else setError("That code isn't valid.");
  };

  return (
    <section className="w-full max-w-[420px] rounded-lg border border-border bg-bg p-5">
      <h3 className="text-sm font-medium">Order summary</h3>
      <ul className="mt-4 grid gap-3.5">
        {items.map((i) => (
          <li key={i.id} className="flex items-start gap-3 text-[13.5px]">
            <span className="grid size-10 shrink-0 place-items-center rounded bg-surface-2 text-fg-muted">{i.icon ?? i.name[0]}</span>
            <div className="min-w-0 flex-1">
              <p className="font-medium">{i.name}</p>
              <p className="text-[12.5px] tabular-nums text-fg-subtle">{i.detail ?? i.qty + " × " + inr(i.unitPrice)}</p>
            </div>
            <span className="tabular-nums">{inr(i.qty * i.unitPrice)}</span>
          </li>
        ))}
      </ul>

      <div className="mt-4 flex gap-2">
        <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Coupon code" aria-label="Coupon code"
          aria-invalid={error ? true : undefined}
          className="h-9 min-w-0 flex-1 rounded border border-border-strong bg-bg px-3 text-sm uppercase outline-none placeholder:normal-case placeholder:text-fg-subtle focus:border-primary focus:ring-[3px] focus:ring-primary/20" />
        <button onClick={apply} className="h-9 rounded border border-border-strong px-3.5 text-sm font-medium hover:bg-surface">Apply</button>
      </div>
      {error && <p className="mt-1.5 text-xs text-danger">{error}</p>}
      {applied && (
        <button onClick={() => setApplied(undefined)} className="mt-2 inline-flex h-[22px] items-center gap-1 rounded-full bg-success-soft px-2 text-xs font-medium text-success-fg"
          aria-label={"Remove coupon " + applied}>
          <Ticket className="size-3" aria-hidden /> {applied} · {coupons[applied]}% off ×
        </button>
      )}

      <dl className="mt-4 grid gap-2 border-t border-border pt-4 text-[13.5px] [&_dd]:tabular-nums">
        <div className="flex justify-between"><dt className="text-fg-muted">Subtotal</dt><dd>{inr(subtotal)}</dd></div>
        {discount > 0 && <div className="flex justify-between"><dt className="text-fg-muted">Discount</dt><dd className="text-success-fg">−{inr(discount)}</dd></div>}
        <div className="flex justify-between"><dt className="text-fg-muted">GST ({gstRate * 100}%)</dt><dd>{inr(gst)}</dd></div>
        <div className="mt-1 flex justify-between border-t border-border pt-3 text-base font-medium"><dt>Total</dt><dd>{inr(total)}</dd></div>
      </dl>

      <button onClick={() => onPay?.(total, applied)}
        className="mt-4 inline-flex h-11 w-full items-center justify-center gap-2 rounded bg-primary text-[15px] font-medium text-primary-fg hover:bg-primary-hover">
        <Lock className="size-4" aria-hidden /> Pay {inr(total)}
      </button>
      <p className="mt-2.5 flex items-center justify-center gap-1.5 text-[12.5px] text-fg-subtle">
        <ShieldCheck className="size-3.5" aria-hidden /> Secure payment · GST invoice emailed to you
      </p>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.