How to Generate a PDF in JavaScript (React)

How to generate a PDF in JavaScript with Spire.PDF for JavaScript in a React application

JavaScript can generate PDF files programmatically by creating a document, adding pages, drawing text and other elements, and saving the document. In a React application, this workflow can run entirely in the browser with Spire.PDF for JavaScript and WebAssembly, without requiring a backend server.

At a high level, JavaScript PDF generation consists of five steps: create the document, add pages, add content, save the PDF, and download the generated file. This tutorial uses React as the example environment, but the PDF-generation workflow itself is JavaScript-based and applies to any frontend framework.

Here, “generating a PDF” means creating a PDF document programmatically from scratch, rather than printing an existing HTML page to PDF. JavaScript applications can generate PDFs through HTML-to-PDF conversion, canvas-based rendering, or programmatic PDF construction. This tutorial focuses on programmatic PDF generation with Spire.PDF for JavaScript.


What You Need

This tutorial assumes that Spire.PDF for JavaScript has already been installed and initialized in your React project. For setup details, see How to Integrate Spire.PDF for JavaScript in a React Project. You also need the WebAssembly resources configured so that the PDF module is available through window.spirepdf (Spire.Office for JavaScript 11.7.0 or later).


How to Generate a PDF in JavaScript

Create PdfDocument  ->  Add Page  ->  Draw Content  ->  Save  ->  Download

1. Create a PDF document

Start by creating a PdfDocument object. This is the in-memory representation of the PDF file you will build.

const pdf = window.spirepdf;
let doc = new pdf.PdfDocument();

2. Add a page

A PDF document needs at least one page. Call Pages.Add() to create a blank page with a default size.

let page = doc.Pages.Add();

3. Add text and other content

The page has a Canvas property that exposes drawing methods. Use DrawString to add text, DrawImage to add images, DrawLine to draw lines, and DrawRectangle to draw filled or outlined rectangles. Coordinates are measured in points (1 point = 1/72 inch) from the top-left corner.

let font = new pdf.PdfFont({ fontFamily: pdf.PdfFontFamily.Helvetica, size: 12 });
let brush = new pdf.PdfSolidBrush({ pdfRGBColor: new pdf.PdfRGBColor(0, 0, 0) });
page.Canvas.DrawString({ s: "Hello, World!", font: font, brush: brush, x: 50, y: 50 });

4. Save the generated PDF

Once all content is in place, call SaveToFile to write the PDF to the WebAssembly virtual file system (VFS). The VFS is an in-browser file system that Spire.PDF uses to manage input and output files without touching the real disk or a server.

doc.SaveToFile({ fileName: 'Output.pdf' });
doc.Close();

5. Download the PDF in the browser

After saving to the VFS, read the file back as a byte array, wrap it in a Blob, and trigger a download by creating a temporary anchor element. This pattern is used in every Spire.PDF for JavaScript example.

const fileArray = window.dotnetRuntime.Module.FS.readFile('Output.pdf');
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'Output.pdf';
a.click();
URL.revokeObjectURL(url);

Programmatic PDF generation is useful when you need precise control over the document structure, page layout, text, graphics, or metadata. If you already have a web page or HTML template and simply need to convert that layout into a PDF, an JavaScript HTML-to-PDF workflow may be a better fit.

The next section puts these steps together in a complete example that generates a PDF from JavaScript data.


Generate an Invoice PDF from JavaScript Data

Real applications generate structured documents — invoices, reports, quotations — from application data, not hardcoded strings. In a React application, the invoice object could come from form input, component state, or an API response; the PDF generation code only needs the data needed for the document layout. This example defines an invoice as a JavaScript object and renders a print-ready PDF from it: a bleed header, a billed-to block, a line-item table, a totals cascade, and payment details. It combines DrawString, DrawRectangle, DrawLine, and PdfGrid into one complete runnable React component.

function App() {
  const generateInvoicePdf = async () => {
    // spire.office 11.7.0 hangs the engine off window.spirepdf.
    const pdf = window.spirepdf;
    if (!pdf) return alert('Spire.PDF is not ready yet');

    // ── The invoice, as data. Swap this object and the layout follows. ────
    const invoice = {
      number: 'INV-2026-0148', issued: '23 September 2026',
      due: '23 October 2026', po: 'PO-ACME-88231', terms: 'Net 30',
      seller: ['NIMBUS SOFTWARE LTD.', 'Cloud platforms for manufacturing teams',
        '12 Kingsway, London WC2B 6UN, United Kingdom'],
      buyer: ['Acme Manufacturing Co.', 'Attn: Sarah Chen, Procurement',
        '1400 Harbor Boulevard, Suite 320', 'Long Beach, CA 90802', 'United States'],
      // [description, quantity, unit price] — the amount is derived below
      items: [
        ['CloudDesk Pro — annual subscription (25 seats)', 25, 120],
        ['Implementation & onboarding (remote, 16 hours)', 16, 95],
        ['Priority support — Premium tier (12 months)', 1, 2400],
        ['Additional object storage (500 GB per year)', 1, 480],
      ],
      discountRate: 0.05, taxRate: 0.08,
      bank: ['Barclays Bank PLC · Sort code 20-00-00 · Account 5571 2043',
        'IBAN GB29 BARC 2000 0055 7120 43 · SWIFT BARCGB22'],
      remark: ['Quote the invoice number as the payment reference.',
        'Amounts unpaid after the due date accrue interest at 1.5% per month.'],
      legal:
        'Nimbus Software Ltd. · 12 Kingsway, London WC2B 6UN · VAT GB 412 8876 21 · Company No. 09882417',
      contact: '[email protected] · +44 20 7946 0812 · nimbussoftware.com',
    };

    const round = (n) => Math.round(n * 100) / 100;
    const subtotal = round(invoice.items.reduce((sum, [, qty, unit]) => sum + qty * unit, 0));
    const discount = -round(subtotal * invoice.discountRate);
    const tax = round((subtotal + discount) * invoice.taxRate);
    const total = round(subtotal + discount + tax);
    const money = (n) =>
      '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    const pct = (rate) => Math.round(rate * 100) + '%';

    // ── Document ──────────────────────────────────────────────────────────
    const doc = new pdf.PdfDocument();
    doc.PageSettings.Margins.All = 0; // before Pages.Add(), or the origin stays at the margin
    const page = doc.Pages.Add();

    // Text is dropped past the canvas client area, so lay out against it.
    const W = page.Canvas.ClientSize.Width;
    const H = page.Canvas.ClientSize.Height;
    const PAD = 28;
    const EDGE = W - PAD;

    // ── Drawing toolkit ───────────────────────────────────────────────────
    // Object notation selects the PdfFont / PdfSolidBrush / PdfPen overload —
    // the positional constructors all report "Ambiguous call" here.
    const rgb = (c) => new pdf.PdfRGBColor(c[0], c[1], c[2]);
    const paint = (c) => new pdf.PdfSolidBrush({ pdfRGBColor: rgb(c) });
    const font = (size, bold) =>
      new pdf.PdfFont({
        fontFamily: pdf.PdfFontFamily.Helvetica,
        size,
        ...(bold ? { style: pdf.PdfFontStyle.Bold } : {}),
      });
    // DrawString's first parameter is `s`, and it has no alignment option in
    // point mode — measure the string when it has to be right-aligned.
    const text = (s, f, colour, x, y, align) => {
      const dx = align === 'right' ? f.MeasureString({ text: s }).Width : 0;
      page.Canvas.DrawString({ s, font: f, brush: paint(colour), x: x - dx, y });
    };
    const box = (colour, x, y, w, h) =>
      page.Canvas.DrawRectangle({ brush: paint(colour), x, y, width: w, height: h });

    const INK = [26, 31, 43], NAVY = [23, 54, 93], ACCENT = [47, 111, 181];
    const MUTED = [107, 114, 128], RULE = [220, 225, 232], ZEBRA = [246, 248, 251];
    const SOFT = [238, 243, 249], WHITE = [255, 255, 255], ON_NAVY = [186, 200, 220];
    const fHero = font(26, true), fBrand = font(17, true), fTitle = font(11, true);
    const fBody = font(9.5), fBold = font(9.5, true), fSmall = font(8.5);
    const fLabel = font(7.5, true), fFoot = font(7.5), fCell = font(9), fHead = font(8, true);

    // ── Header band, bleeding to the sheet edges ──────────────────────────
    box(NAVY, 0, 0, W, 106);
    box(ACCENT, 0, 106, W, 3.5);
    text(invoice.seller[0], fBrand, WHITE, PAD, 30);
    text(invoice.seller[1], fSmall, ON_NAVY, PAD, 52);
    text(invoice.seller[2], fFoot, ON_NAVY, PAD, 68);
    text('INVOICE', fHero, WHITE, EDGE, 22, 'right');
    text(invoice.number, fBody, ON_NAVY, EDGE, 56, 'right');
    text(`Issued ${invoice.issued}`, fFoot, ON_NAVY, EDGE, 74, 'right');

    // ── Billed to / invoice details ───────────────────────────────────────
    const top = 158;
    text('BILL TO', fLabel, MUTED, PAD, top);
    text(invoice.buyer[0], fBold, INK, PAD, top + 17);
    invoice.buyer.slice(2).forEach((line, i) => text(line, fBody, MUTED, PAD, top + 35 + i * 14));
    text(invoice.buyer[1], fSmall, MUTED, PAD, top + 83);

    const details = [
      ['Invoice No.', invoice.number], ['Issue date', invoice.issued], ['Due date', invoice.due],
      ['PO number', invoice.po], ['Payment terms', invoice.terms],
    ];
    text('INVOICE DETAILS', fLabel, MUTED, EDGE, top, 'right');
    details.forEach(([key, value], i) => {
      text(key, fSmall, MUTED, EDGE - 128, top + 20 + i * 16);
      text(value, fBold, INK, EDGE, top + 20 + i * 16, 'right');
    });

    const tableY = top + 106;
    page.Canvas.DrawLine({
      pen: new pdf.PdfPen({ pdfRGBColor: rgb(RULE), width: 0.75 }),
      x1: PAD, y1: tableY, x2: EDGE, y2: tableY,
    });

    // ── Line items ────────────────────────────────────────────────────────
    const grid = new pdf.PdfGrid();
    grid.Columns.Add(4);
    [255, 44, 112, 128].forEach((w, i) => (grid.Columns.get_Item(i).Width = w));

    const alignRight = new pdf.PdfStringFormat({ alignment: pdf.PdfTextAlignment.Right });
    [1, 2, 3].forEach((i) => (grid.Columns.get_Item(i).Format = alignRight));

    // Cell padding is subtracted from row.Height — leave room for one line box
    // or every cell renders blank, with no error at all.
    const padding = new pdf.PdfPaddings();
    padding.Left = padding.Right = 8;
    padding.Top = padding.Bottom = 2;
    grid.Style.CellPadding = padding;
    grid.Style.Font = fCell;

    const hairline = new pdf.PdfBorders();
    hairline.All = new pdf.PdfPen({ pdfRGBColor: rgb(RULE), width: 0.5 });

    try { grid.Headers.Add(1); } catch {}
    const head = grid.Headers.get_Item(0);
    head.Height = 26;
    head.Style.BackgroundBrush = paint(NAVY);
    head.Style.TextBrush = paint(WHITE);
    head.Style.Font = fHead;
    ['DESCRIPTION', 'QTY', 'UNIT PRICE', 'AMOUNT'].forEach((label, i) => {
      const cell = head.Cells.get_Item(i);
      cell.Value = new pdf.String(label); // .Value is a .NET object — box the string
      if (i) cell.StringFormat = alignRight;
      cell.Style.Borders = hairline;
    });

    invoice.items.forEach(([description, qty, unit], r) => {
      const row = grid.Rows.Add();
      row.Height = 26;
      if (r % 2) row.Style.BackgroundBrush = paint(ZEBRA);
      [description, String(qty), money(unit), money(qty * unit)].forEach((value, i) => {
        const cell = row.Cells.get_Item(i);
        cell.Value = new pdf.String(value);
        cell.Style.Borders = hairline;
      });
    });

    const layout = new pdf.PdfGridLayoutFormat();
    layout.Layout = pdf.PdfLayoutType.Paginate;
    // The parameter is `format`, not `layout`.
    const tableBottom = grid.Draw({ page, x: PAD, y: tableY + 26, format: layout }).Bounds.Bottom;

    // ── Totals cascade ────────────────────────────────────────────────────
    let y = tableBottom + 22;
    const summary = [
      ['Subtotal', money(subtotal)],
      [`Discount · partner rate ${pct(invoice.discountRate)}`, '-' + money(discount)],
      [`Sales tax · ${pct(invoice.taxRate)}`, money(tax)],
    ];
    summary.forEach(([label, value], i) => {
      text(label, fBody, MUTED, EDGE - 220, y + i * 18);
      text(value, fBody, INK, EDGE, y + i * 18, 'right');
    });
    y += 58;

    box(NAVY, EDGE - 220, y, 220, 34);
    text('TOTAL DUE', fTitle, WHITE, EDGE - 204, y + 11);
    text(money(total), font(14, true), WHITE, EDGE - 14, y + 8, 'right');
    y += 34;

    // ── Payment details ───────────────────────────────────────────────────
    const cardTop = y + 34;
    box(SOFT, PAD, cardTop, W - 2 * PAD, 104);
    box(ACCENT, PAD, cardTop, 3, 104);
    text('PAYMENT DETAILS', fLabel, ACCENT, PAD + 18, cardTop + 16);
    text(invoice.bank[0], fBody, INK, PAD + 18, cardTop + 34);
    text(invoice.bank[1], fBody, INK, PAD + 18, cardTop + 50);
    invoice.remark.forEach((line, i) => text(line, fSmall, MUTED, PAD + 18, cardTop + 72 + i * 14));

    // ── Footer band, mirroring the header ─────────────────────────────────
    box(NAVY, 0, H - 58, W, 58);
    text(invoice.legal, fFoot, ON_NAVY, PAD, H - 41);
    text(invoice.contact, fFoot, ON_NAVY, PAD, H - 27);
    text('Page 1 of 1', fFoot, ON_NAVY, EDGE, H - 41, 'right');
    text(invoice.number, fFoot, ON_NAVY, EDGE, H - 27, 'right');

    // ── Save and download ─────────────────────────────────────────────────
    const fileName = 'Invoice.pdf';
    doc.SaveToFile({ fileName });
    doc.Close();

    const bytes = window.dotnetRuntime.Module.FS.readFile(fileName);
    const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
    Object.assign(document.createElement('a'), { href: url, download: fileName }).click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Generate Invoice PDF</h1>
      <button onClick={generateInvoicePdf}>Generate Invoice PDF</button>
    </div>
  );
}

export default App;

Invoice PDF generated from JavaScript data

Invoice PDF generated from JavaScript data

What the code does:

  • Defines an invoice object with the seller, the customer, the invoice metadata, and an items array — the shape a React app would receive from a form, an API response, or component state.
  • Derives subtotal, discount, tax, and total from that data with reduce, so the figures printed on the PDF always match the figures in the app.
  • Sets Margins.All to 0 before calling Pages.Add(). The canvas origin sits at the page's margin origin, so this is what makes (0, 0) the top-left corner of the sheet and lets the header and footer bands bleed to the edges.
  • Lays the page out against Canvas.ClientSize instead of Size, because text drawn past the client area is dropped rather than clipped.
  • Draws the header band, the billed-to and invoice-details blocks, the totals cascade, and the payment box with DrawRectangle, DrawLine, and DrawString. Right-aligned strings are measured with MeasureString first, since DrawString has no alignment option when you draw at a point.
  • Builds the line-item table with PdfGrid: fixed column widths, a styled header row, zebra-striped body rows, and right-aligned numeric columns driven by a PdfStringFormat.
  • Saves the document to the VFS and downloads it with the blob-and-anchor pattern from step 5.

The same pattern works for any structured document — replace the invoice object with report data, quotation data, or receipt data, and the five-step workflow stays the same.


Add More Content to the PDF

After creating the basic document, you can extend the same workflow with other PDF elements depending on the document you need to generate.

Element Typical use Tutorial
Text Titles, labels, paragraphs Covered in this tutorial
Images Logos, signatures, charts Add Images to a PDF
Tables Invoices, reports, statements Document Operation guide
Shapes Borders, separators, diagrams Draw Shapes in PDF
Headers & Footers Page numbers, repeating headers Page Setting guide
Form fields Interactive forms, editable documents Form Field guide

The five-step workflow does not change — only the content you draw in step 3 differs.


Generate PDFs in the Browser with WebAssembly

Spire.PDF for JavaScript runs its PDF processing engine through WebAssembly. Once the WASM module loads, your application can create, edit, and save PDFs on the client side. Files move through an in-browser Virtual File System (VFS):

JavaScript  ->  Spire.PDF WebAssembly  ->  VFS  ->  Blob  ->  Download

Spire.PDF for JavaScript PDF - Processing Workflow

This means no PDF-generation backend is required. The browser handles document creation, rendering, and file output locally. The generated PDF is read from the VFS as a byte array and downloaded as a standard Blob.

The trade-off is the initial WASM download size, which is a one-time cost per session. For applications that generate documents frequently, subsequent generations are fast since the module is already loaded. For very large or complex PDFs, client-side performance depends on the user's device and available memory.


Common Use Cases

Common use cases include invoices, reports, quotations, certificates, receipts, and other structured business documents generated from application data. The specific layout changes, but the underlying workflow — create, add page, draw, save, download — remains the same.


Troubleshooting

WASM Module Not Initialized

If window.spirepdf is undefined, ensure the WebAssembly runtime is fully initialized before using the API:

const commonModule = await import('/node_modules/spire.office/spire.common.js');
await commonModule.initializeWasm();
await import('/node_modules/spire.office/spire.pdf.js');

Generated PDF Cannot Be Downloaded

If the download does not trigger or the file is empty, verify that SaveToFile was called before reading from the VFS. The file must exist in the VFS before FS.readFile can read it:

doc.SaveToFile({ fileName: 'Output.pdf' });
doc.Close();

// Only read after saving
const fileArray = window.dotnetRuntime.Module.FS.readFile('Output.pdf');

Conclusion

Spire.PDF for JavaScript provides a practical way to generate PDF files directly in the browser using JavaScript and WebAssembly. The five-step workflow — create, add page, draw, save, download — handles everything from simple text documents to structured invoices with tables and totals. You can apply for a 30-day free license to evaluate all features before purchasing.