Invoice Card
NewAn invoice summary with line items, a status badge that animates between draft, sent, overdue and paid, totals that roll, a hold to confirm Mark as paid, and download and share actions.
motionlucide-reactutilsbadgebuttoncarditemseparatorspinner@soultel/rolling-number@soultel/icon-download@soultel/use-reduced-motion-safe
Preview
Installation
$ pnpm dlx shadcn@latest add @soultel/invoice-cardNo namespace set up? npx shadcn@latest add https://asafefe.dev/r/invoice-card.json works too.
Manual installation
- 1. Install the dependencies.
terminal1 lines npm install motion lucide-react - 2. Copy the source from the Code tab into components/invoice-card.tsx.
Usage
import { InvoiceCard, type Invoice } from "@/components/invoice-card"
const invoice: Invoice = {
number: "INV-0042",
from: "Fixwell Repairs",
customer: { name: "Dana Whitfield", email: "dana@whitfield.co" },
issuedAt: "2026-10-20",
dueAt: "2026-11-03",
taxRate: 8,
lines: [
{ id: "screen", description: "iPhone 15 screen replacement", quantity: 1, unitPrice: 229 },
{ id: "labor", description: "Labor", detail: "1.5 hours at $60", quantity: 1.5, unitPrice: 60 },
],
}
export function JobInvoice() {
return (
<InvoiceCard
invoice={invoice}
defaultStatus="sent"
onMarkPaid={async (inv, totals) => {
await fetch(`/api/invoices/${inv.number}/pay`, { method: "POST", body: JSON.stringify(totals) })
}}
onDownload={(inv) => window.open(`/invoices/${inv.number}.pdf`)}
shareUrl="https://pay.fixwell.app/i/INV-0042"
/>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| invoice | Invoice | - | number, customer { name, email }, issuedAt and dueAt as "YYYY-MM-DD", lines { id, description, quantity, unitPrice, detail }[], and optional from, taxRate (percent), discount and paidAt. |
| status / defaultStatus | "draft" | "sent" | "overdue" | "paid" | "sent" | Controlled or initial status. |
| onStatusChange | (status: InvoiceStatus) => void | - | Called after a send or a recorded payment. |
| onSend | (invoice) => void | Promise<void> | - | Shown on drafts. On success the status moves to sent. |
| onMarkPaid | (invoice, totals) => void | Promise<void> | - | Called once the payment is confirmed. A promise shows a pending state; a rejection keeps the status and shows an error. |
| confirm | "hold" | "click" | "hold" | Press and hold, or a second click, to mark as paid. |
| holdDuration | number | 1200 | Hold time in ms. |
| onDownload | (invoice) => void | - | Called by Download PDF. |
| onShare / shareUrl | (invoice) => void / string | - | Replace the default share, which opens the share sheet on touch devices and copies shareUrl (or the page URL) elsewhere. |
| currency | string | "$" | Symbol shown before amounts. |
| locale | string | "en-US" | Used for dates. |
