DEV Community

Jack Green
Jack Green

Posted on • Originally published at invoice.jackgreen.top

Building a 100% Client-Side PDF Invoice Generator with jsPDF (No Backend, No Sign-Up)

Freelancers send a handful of invoices a month, yet most invoicing tools charge a monthly subscription. I wanted the opposite: a tool you open in a browser, fill in, and get a professional PDF — no account, no server, no data leaving your machine.

Here's how I built it, and the interesting problems along the way.

The constraint that shaped everything: no backend

The entire app is static files: one HTML page, some CSS, and four small JS modules. PDF generation happens in the browser with jsPDF plus the autoTable plugin for line items.

That constraint buys three things for free:

  • Privacy — client names and amounts never touch a server.
  • Offline — it works with WiFi off; useful on trains and planes.
  • Zero hosting cost — it's static, so it deploys anywhere.

Generating the PDF

The core is surprisingly small. jsPDF gives you a canvas-like imperative API, and autoTable handles the line-item table with automatic pagination:

const doc = new jspdf.jsPDF();
doc.setFontSize(22);
doc.text('INVOICE', 14, 22);

doc.autoTable({
  startY: 60,
  head: [['Description', 'Qty', 'Rate', 'Amount']],
  body: items.map(i => [i.desc, i.qty, fmt(i.rate), fmt(i.qty * i.rate)]),
  theme: 'striped',
});

doc.save(`invoice-${number}.pdf`);
Enter fullscreen mode Exit fullscreen mode

The details that took actual time:

  • Currency formattingIntl.NumberFormat handles symbols and thousands separators correctly for every locale, so don't hand-roll it.
  • Long descriptions — autoTable wraps cell text, but you need columnStyles with fixed widths or totals drift off the page edge.
  • Totals section — drawn manually after the table using doc.lastAutoTable.finalY as the anchor.

Templates with localStorage

Freelancers invoice the same clients repeatedly, so the app saves templates as plain JSON in localStorage — again, no server:

localStorage.setItem('invoice-template-' + name, JSON.stringify(formState));
Enter fullscreen mode Exit fullscreen mode

One catch: localStorage is per-origin, so if you later move domains, saved templates don't follow. Export/import as a JSON file covers backup.

Dark mode without a toggle

CSS custom properties plus prefers-color-scheme means the app follows the OS setting with zero JS:

:root { --bg: #ffffff; --ink: #111827; }
@media (prefers-color-scheme: dark) {
  :root { --bg: #0f172a; --ink: #e2e8f0; }
}
Enter fullscreen mode Exit fullscreen mode

The PDF itself always renders light — invoices get printed, and toner is expensive.

The business experiment

I'm selling this the boring way: a $9 one-time purchase (via Creem, a merchant-of-record, so VAT is handled). The free demo generates 5 watermarked invoices; the paid version is the same app, unlocked, delivered as a ZIP you own forever. No subscription, because charging monthly for a tool that runs entirely on your computer feels wrong.

If you try it, I'd genuinely like to hear what breaks or what's missing — comments here are perfect.

Top comments (0)