asafefe.dev

Invoice Card

New

An 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
Fixwell Repairs
Invoice INV-0042
Sent

Amount due

$440.64

Due Nov 3

Billed to
Dana Whitfield
dana@whitfield.co
Issued
Oct 20, 2026
Due Nov 3, 2026
iPhone 15 screen replacement

OEM panel, 1 year warranty

$229.00
Battery replacement

1 × $89.00

$89.00
Labor

1.5 hours at $60.00

$90.00
Subtotal
$408.00
Tax (8%)
$32.64
Total
$440.64
Amount due
$440.64
Preview

Installation

$ pnpm dlx shadcn@latest add @soultel/invoice-card

No namespace set up? npx shadcn@latest add https://asafefe.dev/r/invoice-card.json works too.

Manual installation
  1. 1. Install the dependencies.
    terminal1 lines
    npm install motion lucide-react
  2. 2. Copy the source from the Code tab into components/invoice-card.tsx.

Usage

example.tsx28 lines
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

PropTypeDefaultDescription
invoiceInvoice-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.
holdDurationnumber1200Hold 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.
currencystring"$"Symbol shown before amounts.
localestring"en-US"Used for dates.