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
| Description | Qty | Rate | Amount |
|---|---|---|---|
| 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.