Tutorial

Building a PDF Invoice Generator With Zero Libraries

window.print() is massively underrated. Here is how I used it to build a professional PDF invoice generator.

When I was building the Invoice Generator for my developer tools directory, I hit a massive technical roadblock early on: How do I export a dynamic HTML invoice layout into a perfectly formatted, professional, and downloadable PDF document?

I searched StackOverflow, Reddit, and YouTube. Every single tutorial, forum answer, and AI prompt told me the exact same thing: "Just npm install html2pdf.js, use jsPDF, or bundle html2canvas."

But adding 500kb of bloated external libraries to my web application just to print a document went against everything my portfolio stands for. I wanted a lightning-fast application with zero dependencies. More importantly, I wanted a solution that didn't compromise on quality.

In this comprehensive, deep-dive tutorial, I will show you exactly how to generate flawless, vector-crisp PDFs using nothing but native browser APIs and the incredibly powerful (and highly underrated) @media print CSS query. We will cover the pitfalls of canvas-based libraries, advanced CSS layout techniques for printing, handling dynamic page breaks, and how to trigger the print sequence using Vanilla JavaScript.

The Trap of PDF JavaScript Libraries (Why html2canvas Fails)

Before we look at the native solution, we must fundamentally understand why the standard "npm install" approach is deeply flawed for document generation. Libraries like html2pdf.js and html2canvas work by using a technique called DOM rasterization.

Essentially, they traverse your HTML DOM, draw every element onto an invisible HTML5 <canvas> element, take a literal "screenshot" of that canvas (converting it into a massive PNG or JPEG image), and then paste that static image into a PDF wrapper.

This rasterization approach creates four massive problems for professional applications:

  1. Blurry, Rasterized Text: Because the final output is a flat image, the text is no longer scalable or vector-based. If a client zooms in on the PDF to read the fine print, the text looks pixelated, jagged, and highly unprofessional.
  2. Loss of Interactivity (No Text Selection): Because the text is baked into an image, the client cannot highlight, copy, or paste the invoice number, the routing number, or the bank details from the PDF into their accounting software.
  3. Massive File Sizes: A high-resolution, image-based PDF can easily exceed 2MB to 5MB per page. In contrast, a native, text-based PDF generated by the browser is usually under 50KB.
  4. Mobile Performance Issues: Rendering complex DOM trees onto a canvas requires massive amounts of CPU and RAM. On older mobile devices, generating a 3-page canvas-based PDF will often crash the browser tab entirely.

The Native Solution: The power of window.print()

It turns out that modern browsers—Chrome, Safari, Firefox, and Edge—already have a world-class, highly optimized, vector-based PDF generation engine built right in: the native Print Dialog. You can trigger this engine programmatically using a single line of JavaScript:

javascript
// Trigger the browser's native print / save-as-pdf dialog
document.getElementById('download-pdf-btn').addEventListener('click', () => {
    window.print();
});

The problem? If you run this line of code right now, the browser literally attempts to print your entire website exactly as it appears on the screen. It will include your sticky navigation bar, your interactive "Download" buttons, your dark-mode toggle, your footer, and it will likely chop your content in half awkwardly across two pages. It looks terrible.

To fix this, we don't need heavy JavaScript logic. We need highly specific, print-targeted CSS.

Mastering the @media print CSS Query

We need to write a stylesheet that acts as a set of instructions, telling the browser exactly how to behave when it switches from "Screen Mode" to "Print Mode". The @media print query allows us to hide the UI layer completely and isolate just the invoice container.

css
/* This CSS ONLY applies when the user clicks Print or Save as PDF */
@media print {
    /* 1. Hide the entire UI (Navbars, Footers, Settings Panels, Buttons) */
    body * {
        visibility: hidden;
    }
    
    /* 2. Make ONLY the invoice container and its children visible */
    #invoice-container, #invoice-container * {
        visibility: visible;
    }
    
    /* 3. Position the invoice at the absolute top-left of the physical paper */
    #invoice-container {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        margin: 0;
        padding: 0;
        
        /* Remove web-specific styles that look bad on physical paper */
        box-shadow: none !important; 
        border: none !important;
        border-radius: 0 !important;
    }

    /* 4. Ensure the body doesn't add extra scrolling margins */
    body {
        margin: 0;
        padding: 0;
        background: white; /* Force a white background for the paper */
    }
}

Fixing Browser Quirks: Background Colors & Margins

If you test the code above, you will notice a frustrating browser quirk: your beautiful colored table headers, your brand logos, and your "Paid" status badges have turned completely white. By default, Chrome and Safari aggressively strip out all background colors and background images during printing. This is a legacy feature designed to save the user's expensive printer ink.

Since our users are saving this as a digital PDF (not physically printing it on paper), we need to override this behavior and force the browser to render our colors using the print-color-adjust property.

css
@media print {
    /* Force background colors, gradients, and images to render natively */
    * {
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
        color-adjust: exact !important;
    }

    /* Define the physical paper size and default margins */
    @page {
        size: A4 portrait;
        margin: 1.5cm; /* Gives the PDF professional, consistent breathing room */
    }
}

Handling Page Breaks Like a Senior Engineer

When generating dynamic business documents like invoices or reports, you must ensure the layout adapts perfectly to the data. What happens if a client has 30 individual line items on their invoice? It will overflow onto a second page.

If you don't handle page breaks explicitly, the browser might slice a row of text, a table cell, or a signature block cleanly in half between Page 1 and Page 2. We handle this elegantly using CSS page-break properties.

css
@media print {
    /* Prevent table rows and specific blocks from splitting across two pages */
    tr, .invoice-line-item, .signature-block {
        page-break-inside: avoid;
        break-inside: avoid; /* Modern syntax */
    }
    
    /* Force a clean page break BEFORE a specific element (like a Terms of Service page) */
    .terms-and-conditions {
        page-break-before: always;
        break-before: page;
    }

    /* Ensure Headings stay with their following paragraphs */
    h1, h2, h3, h4, h5 {
        page-break-after: avoid;
        break-after: avoid;
    }
}

Advanced: Modifying the DOM Before Printing

Sometimes CSS isn't enough. You might need to change the document title so that when the user saves the PDF, the default filename is Invoice_1042.pdf instead of My_Website_Name.pdf. You can hook into the print events using JavaScript.

javascript
function generatePDF(invoiceNumber) {
    // Save the original document title
    const originalTitle = document.title;
    
    // Change the title temporarily to format the default PDF filename
    document.title = `Invoice_${invoiceNumber}`;
    
    // Trigger the print dialog
    window.print();
    
    // Listen for the print dialog closing to restore the original title
    window.addEventListener('afterprint', () => {
        document.title = originalTitle;
    }, { once: true });
}

Frequently Asked Questions (FAQ)

Can I auto-save the PDF instead of forcing the user to open the print dialog?

No. For strict security and anti-malware reasons, modern web browsers do not allow JavaScript to silently download files, execute scripts, or save documents to a user's hard drive without their explicit, manual interaction. The window.print() dialog is the required native security step. However, users can simply select "Save as PDF" from the system dropdown menu.

Is this native printing method mobile-friendly?

Yes, absolutely! iOS Safari and Android Chrome handle window.print() exceptionally well. It allows mobile users to generate the invoice and immediately save the resulting PDF directly to their Apple Files app, Google Drive, or share it directly with a client via WhatsApp or Email.

What about custom web fonts (like Google Fonts) in the PDF?

This is one of the biggest advantages of this method. Because the browser is rendering the PDF natively using its own highly-advanced typography engine, any web fonts (like Google Fonts, Adobe Fonts, or custom WOFF2 files) that have fully loaded on the HTML page will be automatically embedded directly into the resulting PDF. This results in infinitely scalable, highly selectable text that looks identical to your web design.

How do I hide URLs from printing at the bottom of the page?

By default, browsers often print the page URL and the date in the headers and footers. You can disable this by setting the margin in the @page rule, but ultimately, removing headers and footers is a user-controlled setting in the print dialog (usually a checkbox called "Headers and footers"). You should advise your users to uncheck this box for the cleanest output.

Conclusion

By fully leveraging standard, built-in browser features, I was able to build a lightning-fast PDF exporter that renders custom fonts and SVG vectors flawlessly, all while keeping my application's JavaScript bundle size at absolute zero.

Before you blindly run an npm install command to solve a problem, always check the native MDN Web Docs. The modern browser is an incredibly powerful operating system in itself. Sometimes, the absolute best library for the job is no library at all.

Want to see this code in action? Check out the source code of my completely free, client-side PDF Invoice Generator.