Skip to content
All Taste components

Commerce

Invoice preview

A GST invoice as the client will see it, with status, parties, items, tax split and actions.

status badge · from/to · line items · CGST/SGST · payment note · download & send

Saffron Studio
GSTIN 29AAKCS4821M1Z5
Due in 15 days
INV-2041
Billed to
Kaveri Foods Pvt Ltd
12 Sayyaji Rao Road
Mysuru, Karnataka 570001
Issued
15 Oct 2026
Due
30 Oct 2026
Amount due
₹1,00,890
DescriptionQtyRateAmount
Website design, 8 pages
SAC 998391
1₹60,000₹60,000
Product photography
SAC 998386
40₹450₹18,000
Hosting & care, Oct–Dec
SAC 998315
3₹2,500₹7,500
Subtotal₹85,500
CGST 9%₹7,695
SGST 9%₹7,695
Total₹1,00,890
Pay by UPI to saffron@upi or bank transfer. Thank you for your business.
import { Download, Printer, Send } from "lucide-react";

export interface InvoiceLine { description: string; code?: string; qty: number; rate: number }
export interface Invoice {
  number: string;
  issued: string;
  due: string;
  status: "draft" | "due" | "paid" | "overdue";
  from: { name: string; gstin: string };
  to: { name: string; address: string[] };
  lines: InvoiceLine[];
  /** Intra-state supply splits GST into CGST + SGST; inter-state uses IGST. */
  interState?: boolean;
  gstRate?: number;
  note?: string;
}

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

const sample: Invoice = {
  number: "INV-2041", issued: "15 Oct 2026", due: "30 Oct 2026", status: "due",
  from: { name: "Saffron Studio", gstin: "29AAKCS4821M1Z5" },
  to: { name: "Kaveri Foods Pvt Ltd", address: ["12 Sayyaji Rao Road", "Mysuru, Karnataka 570001"] },
  lines: [
    { description: "Website design, 8 pages", code: "SAC 998391", qty: 1, rate: 60000 },
    { description: "Product photography", code: "SAC 998386", qty: 40, rate: 450 },
    { description: "Hosting & care, Oct–Dec", code: "SAC 998315", qty: 3, rate: 2500 },
  ],
  note: "Pay by UPI to saffron@upi or bank transfer. Thank you for your business.",
};

const status = {
  draft: ["Draft", "bg-surface-2 text-fg-muted"],
  due: ["Due", "bg-warning-soft text-warning-fg"],
  paid: ["Paid", "bg-success-soft text-success-fg"],
  overdue: ["Overdue", "bg-danger-soft text-danger-fg"],
} as const;

export function InvoicePreview({ invoice = sample, onDownload, onSend }:
  { invoice?: Invoice; onDownload?: () => void; onSend?: () => void }) {
  const rate = invoice.gstRate ?? 0.18;
  const subtotal = invoice.lines.reduce((s, l) => s + l.qty * l.rate, 0);
  const tax = subtotal * rate;
  const total = subtotal + tax;
  const [label, tone] = status[invoice.status];
  const cell = "px-3 py-2.5 text-right tabular-nums last:pr-0";

  return (
    <article className="grid w-full max-w-[600px] gap-6 rounded-lg border border-border bg-bg p-7">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div className="flex items-center gap-2.5">
          <span className="grid size-9 place-items-center rounded bg-primary font-semibold text-primary-fg" aria-hidden>{invoice.from.name[0]}</span>
          <div>
            <p className="font-medium">{invoice.from.name}</p>
            <p className="text-[12.5px] text-fg-subtle">GSTIN {invoice.from.gstin}</p>
          </div>
        </div>
        <div className="text-right">
          <span className={"inline-flex h-[22px] items-center gap-1.5 rounded-full px-2 text-xs font-medium " + tone}>
            <span className="size-1.5 rounded-full bg-current" />{label}
          </span>
          <p className="mt-1.5 text-lg font-medium tabular-nums">{invoice.number}</p>
        </div>
      </header>

      <dl className="grid gap-4 text-[13px] [grid-template-columns:repeat(auto-fit,minmax(150px,1fr))]">
        <div>
          <dt className="mb-1 text-xs text-fg-subtle">Billed to</dt>
          <dd><p className="font-medium">{invoice.to.name}</p>{invoice.to.address.map((l) => <p key={l}>{l}</p>)}</dd>
        </div>
        <div>
          <dt className="mb-1 text-xs text-fg-subtle">Issued</dt><dd>{invoice.issued}</dd>
          <dt className="mb-1 mt-2.5 text-xs text-fg-subtle">Due</dt><dd>{invoice.due}</dd>
        </div>
        <div>
          <dt className="mb-1 text-xs text-fg-subtle">Amount due</dt>
          <dd className="text-xl font-medium tracking-tight tabular-nums">{inr(total)}</dd>
        </div>
      </dl>

      <div className="overflow-x-auto">
        <table className="w-full text-[13.5px]">
          <thead>
            <tr className="border-b border-border text-xs text-fg-subtle">
              <th className="py-2 pr-3 text-left font-medium">Description</th>
              <th className="px-3 py-2 text-right font-medium">Qty</th>
              <th className="px-3 py-2 text-right font-medium">Rate</th>
              <th className="py-2 pl-3 text-right font-medium">Amount</th>
            </tr>
          </thead>
          <tbody className="divide-y divide-border">
            {invoice.lines.map((l) => (
              <tr key={l.description}>
                <td className="py-2.5 pr-3">{l.description}{l.code && <p className="text-[12.5px] text-fg-subtle">{l.code}</p>}</td>
                <td className={cell}>{l.qty}</td>
                <td className={cell}>{inr(l.rate)}</td>
                <td className={cell}>{inr(l.qty * l.rate)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <dl className="ml-auto grid w-full max-w-[280px] gap-2 text-[13.5px] [&_dd]:tabular-nums">
        <div className="flex justify-between"><dt className="text-fg-muted">Subtotal</dt><dd>{inr(subtotal)}</dd></div>
        {invoice.interState ? (
          <div className="flex justify-between"><dt className="text-fg-muted">IGST {rate * 100}%</dt><dd>{inr(tax)}</dd></div>
        ) : (
          <>
            <div className="flex justify-between"><dt className="text-fg-muted">CGST {(rate * 100) / 2}%</dt><dd>{inr(tax / 2)}</dd></div>
            <div className="flex justify-between"><dt className="text-fg-muted">SGST {(rate * 100) / 2}%</dt><dd>{inr(tax / 2)}</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>

      <footer className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4 text-[12.5px] text-fg-subtle">
        <p>{invoice.note}</p>
        <div className="flex gap-2">
          <button onClick={() => window.print()} className="inline-flex h-[30px] items-center gap-1.5 rounded border border-border-strong px-2.5 text-[13px] font-medium text-fg hover:bg-surface">
            <Printer className="size-3.5" aria-hidden /> Print
          </button>
          <button onClick={onDownload} className="inline-flex h-[30px] items-center gap-1.5 rounded border border-border-strong px-2.5 text-[13px] font-medium text-fg hover:bg-surface">
            <Download className="size-3.5" aria-hidden /> PDF
          </button>
          <button onClick={onSend} className="inline-flex h-[30px] items-center gap-1.5 rounded bg-primary px-2.5 text-[13px] font-medium text-primary-fg hover:bg-primary-hover">
            <Send className="size-3.5" aria-hidden /> Send invoice
          </button>
        </div>
      </footer>
    </article>
  );
}

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