DEV Community

Shahzaib
Shahzaib

Posted on

Premium Invoice Generator — From Basic to Beautiful (All in the Browser)

Series: Building PdfWord — a free, no-backend PDF tools site (Part 8)

My first invoice generator worked. It produced a correct PDF with correct totals. And it looked… fine. Like a spreadsheet that learned to wear a tie.

Then a user sent me a screenshot of an invoice they'd made for their actual shop — and the "TOTAL DUE" line had a purple divider striking straight through the text. A real business, a real invoice, and my layout bug sitting in the middle of it.

That screenshot redesigned the whole thing. Here's the before-and-after story, plus the pdf-lib techniques behind the premium version.

Try it: Invoice Generator


The bug that started it

The totals section drew a divider line before rendering the TOTAL DUE row:

tr('Discount', -t.disc);
// divider drawn at yy + 8...
pg.drawLine({ start: { x: tx0, y: yy + 8 }, end: { x: tx1, y: yy + 8 }, thickness: 1.5 });
// ...then text drawn AT yy — the line lands mid-glyph on 12pt text
tr('TOTAL DUE', t.grand, true);
Enter fullscreen mode Exit fullscreen mode

Text drawn at baseline yy extends roughly 12pt upward. A line at yy + 8 lands in the middle of the glyphs. On screen it read as a strikethrough. The fix was obvious once seen — but I'd shipped it, QA'd it, and never noticed, because my test data never made me look at that row the way a shop owner does.

The redesign: five decisions

1. The total became a band, not a line. Instead of a divider above "TOTAL DUE," the total now sits on a solid indigo band with white text. No line left to misplace — and it anchors the whole page visually.

2. The table header got inverted. Light header with colored text became a solid indigo header with white text. One drawRectangle, then white drawText on top. It instantly reads as "designed" instead of "default."

3. Zebra striping. Every other row gets a whisper-light background. Three lines of code, enormous readability payoff:

it.forEach((r, i) => {
  if (i % 2 === 1)
    pg.drawRectangle({ x: M, y: yy - 6, width: TW, height: 22, color: LT });
  // ... draw the row's text on top
});
Enter fullscreen mode Exit fullscreen mode

4. The header became a real header. Invoice number, issue date, and due date each got small-caps labels (INVOICE NO., ISSUE DATE, DUE DATE) right-aligned in the band — the way real invoices look, instead of a number floating in the corner.

5. A footer with manners. "Thank you for your business!" centered above the generator credit. Invoices are correspondence; they should sign off properly.

The unglamorous hero: right-alignment math

The fiddliest part of invoice layout is right-aligned numbers. pdf-lib won't do it for you — you measure the text and subtract:

const RX = (s, sz, ft) => W - M - ft.widthOfTextAtSize(String(s), sz);
pg.drawText(amount, { x: RX(amount, 12, boldFont), y, size: 12, font: boldFont });
Enter fullscreen mode Exit fullscreen mode

Every monetary value on the page goes through this helper. Get one wrong and the column wobbles. I use it religiously now.

One encoding gotcha

The pretty minus sign − (U+2212) is not in WinAnsi encoding — pdf-lib's standard Helvetica fonts will choke on it. Use ASCII - in PDFs and save the nice typography for the HTML preview. (The em-dash —, U+2014, is in WinAnsi, so that one's fine.) I keep a mental allowlist now.

The result

Same inputs, same pdf-lib, same browser. The difference is about 80 lines of layout code — and actually looking at the output the way a customer would. The shop owner who sent the screenshot now uses it for real invoices, in PKR with 16% tax.

That's the whole story: correctness gets you shipped, but the last 10% of layout is what makes someone trust it with their business.

Try it: Invoice Generator — PKR, USD, EUR and more, tax + discount, live preview, no signup.

What's the most embarrassing visual bug you've ever shipped? I just showed you mine.

Top comments (0)