Export to PDF
In this tutorial, you will export grid data to a downloadable PDF using jsPDF and jspdf-autotable. You will learn how to read the current grid state with getData() and getColHeader(), build a multi-page PDF table with headers, and trigger a browser download.
/* file: app.component.ts */import { Component, ViewChild } from '@angular/core';import { GridSettings, HotTableComponent, HotTableModule } from '@handsontable/angular-wrapper';import { jsPDF } from 'jspdf';import { autoTable } from 'jspdf-autotable';
/* start:skip-in-preview */const ROWS = 85;const data = Array.from({ length: ROWS }, (_, row) => [ `SKU-${1000 + row}`, `Product ${row + 1}`, row % 12 + 1, (9.99 + row * 0.25).toFixed(2), ((row % 12 + 1) * (9.99 + row * 0.25)).toFixed(2),]);/* end:skip-in-preview */
@Component({ standalone: true, imports: [HotTableModule], selector: 'example1-export-to-pdf', template: ` <div class="example-controls-container"> <div class="controls"> <button type="button" (click)="exportToPdf()">Export to PDF</button> </div> </div> <hot-table [data]="data" [settings]="gridSettings"></hot-table> `,})export class AppComponent { @ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent;
readonly data = data;
readonly gridSettings: GridSettings = { colHeaders: ['SKU', 'Product', 'Qty', 'Unit price', 'Line total'], columnSorting: true, rowHeaders: true, height: 320, width: '100%', };
exportToPdf(): void { const hot = this.hotTable?.hotInstance;
if (!hot) { return; }
const body = hot.getData(); const colCount = hot.countCols(); const head = [ Array.from({ length: colCount }, (_, col) => String(hot.getColHeader(col))), ];
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
autoTable(doc, { head, body, styles: { fontSize: 8, cellPadding: 1.5, overflow: 'linebreak' }, headStyles: { fillColor: [26, 66, 232], textColor: 255, fontStyle: 'bold', }, alternateRowStyles: { fillColor: [245, 247, 250] }, margin: { top: 14, left: 12, right: 12, bottom: 14 }, showHead: 'everyPage', });
doc.save('export.pdf'); }}/* end-file */
/* file: app.config.ts */import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';import { registerAllModules } from 'handsontable/registry';import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
registerAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<div><example1-export-to-pdf></example1-export-to-pdf></div>Overview
This recipe shows how to export the current Handsontable data to a PDF file when the user clicks Export to PDF. The recommended approach is jsPDF with jspdf-autotable, which builds a real PDF table from your data so text stays selectable and accessible.
Difficulty: Beginner Time: ~20 minutes Libraries: jsPDF, jspdf-autotable (via CDN or npm)
What You’ll Build
A button above a Handsontable grid that:
- Reads all rows and columns from the live grid (including any sorting the user applied)
- Builds a multi-page A4 PDF with column headers repeated on every page
- Alternates row background colors for readability
- Triggers a browser download when clicked
Prerequisites
npm install jspdf jspdf-autotableImport the libraries
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { jsPDF } from 'jspdf';import { autoTable } from 'jspdf-autotable';registerAllModules();TypeScript:
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { jsPDF } from 'jspdf';import { autoTable } from 'jspdf-autotable';registerAllModules();What’s happening:
handsontable/baseis the tree-shakeable entry point — it loads only what you registerregisterAllModules()registers every built-in plugin and cell type; scope this down if bundle size mattersjsPDFis the PDF document constructorautoTableis the plugin function that draws a formatted table into the document
Set up Handsontable
const container = document.querySelector('#example1');const hot = new Handsontable(container, {data,colHeaders: ['SKU', 'Product', 'Qty', 'Unit price', 'Line total'],columnSorting: true,rowHeaders: true,height: 320,width: '100%',licenseKey: 'non-commercial-and-evaluation',});TypeScript: Use
document.querySelector('#example1')!(non-null assertion) and type the options asHandsontable.GridSettings.Key points:
colHeadersdefines the visible column labels — you’ll read these back in Step 4 to build the PDF header rowcolumnSorting: truelets users reorder rows before exporting;hot.getData()always returns the current visual orderheight: 320constrains the on-screen grid; it has no effect on how many rows are written to the PDF
Read the grid data
function exportGridToPdf() {const body = hot.getData();// ...}What’s happening:
hot.getData()returns a 2D array of all cell values in the current visual order- If the user sorted column A descending, the rows in
bodymatch what they see on screen — the export reflects the current view, not the original data order - Each inner array is one row:
['SKU-1000', 'Product 1', 1, '9.99', '9.99']
Build the column headers
const colCount = hot.countCols();const head = [Array.from({ length: colCount }, (_, col) => String(hot.getColHeader(col))),];What’s happening:
hot.countCols()returns the number of visible columnshot.getColHeader(col)returns the header label for each column index — this matches thecolHeadersarray you set in Step 2- The result is wrapped in an outer array because
autoTableexpectsheadto be an array of rows (you could pass multiple header rows for grouped headers) String(...)converts numeric auto-generated headers (e.g.,1,2,3) to strings
Why read headers from the grid instead of hardcoding them?
Reading from
hot.getColHeader()means the PDF header stays in sync with the grid automatically — rename a column header in one place and the export picks it up without a separate update.Create the PDF document
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });Options:
Option Value Effect orientation'portrait'Page is taller than it is wide unit'mm'All measurements (margins, padding) are in millimeters format'a4'Standard A4 page (210 x 297 mm) Alternatives:
// Landscape for wide tablesnew jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' })// US Letter sizenew jsPDF({ orientation: 'portrait', unit: 'mm', format: 'letter' })Generate the table with autoTable
autoTable(doc, {head,body,styles: { fontSize: 8, cellPadding: 1.5, overflow: 'linebreak' },headStyles: {fillColor: [26, 66, 232],textColor: 255,fontStyle: 'bold',},alternateRowStyles: { fillColor: [245, 247, 250] },margin: { top: 14, left: 12, right: 12, bottom: 14 },showHead: 'everyPage',});What each option does:
head— the header row built in Step 4; rendered withheadStylesbody— the 2D data array from Step 3; one inner array per rowstyles— applied to every cell:fontSize: 8— 8 pt fits more columns on A4 without wrappingcellPadding: 1.5— 1.5 mm padding keeps rows compactoverflow: 'linebreak'— long text wraps to the next line instead of being clipped
headStyles— overridesstylesfor header cells only:fillColor: [26, 66, 232]— RGB blue backgroundtextColor: 255— white text (shorthand for[255, 255, 255])fontStyle: 'bold'— bold header text
alternateRowStyles— applies to every other body row; the light gray ([245, 247, 250]) makes long tables easier to readmargin— page margins in mm; gives breathing room around the table on all four sidesshowHead: 'everyPage'— repeats the header row at the top of each new page when the table spans multiple pages
Pagination:
autoTablehandles page breaks automatically. Whenbodyhas more rows than fit on one page, it starts a new page and — becauseshowHead: 'everyPage'is set — prints the header row again at the top.Save the PDF file
doc.save('export.pdf');What’s happening:
doc.save()serializes the PDF and triggers a browser download- Pass any filename you want; the
.pdfextension is required for browsers to open it correctly - This is a client-side download — no server involved
Alternative — open in a new tab instead of downloading:
const blob = doc.output('blob');const url = URL.createObjectURL(blob);window.open(url, '_blank');Wire the Export to PDF button
document.querySelector('#exportPdfBtn').addEventListener('click', exportGridToPdf);TypeScript: Use
document.querySelector('#exportPdfBtn')!to satisfy the null check.What’s happening:
#exportPdfBtnmatches the button in the HTML file (see Step 9)- Every click calls
exportGridToPdf(), which always reads the current grid state — sorts, filters, or edits made since the last export are included automatically
Add the button and style it
Add a toolbar above the grid container in your HTML:
<div class="export-pdf-toolbar"><button type="button" id="exportPdfBtn" class="export-pdf-btn">Export to PDF</button></div><div id="example1"></div>Style the button with CSS:
.export-pdf-toolbar {margin-bottom: 12px;}.export-pdf-btn {background: #1a42e8;border: none;border-radius: 6px;color: #fff;cursor: pointer;font-size: 14px;font-weight: 600;padding: 8px 16px;}.export-pdf-btn:hover {background: #1536c4;}What’s happening:
- The button sits in its own
divso you can add more toolbar items later without changing the layout - The button
idmust match the selector used in Step 8 (#exportPdfBtn) - The grid container
idmust match the selector used in Step 2 (#example1) - The hover color (
#1536c4) is a slightly darker shade of the button background to give visual feedback
- The button sits in its own
How it works — complete flow
- Page loads — Handsontable renders the grid with 85 rows of sample data
- User sorts (optional) — clicking a column header reorders rows; the export will reflect the new order
- User clicks Export to PDF — the click event calls
exportGridToPdf() - Read the grid —
hot.getData()returns all rows in current visual order;hot.getColHeader()returns header labels - Build the PDF — a new
jsPDFdocument is created with A4 portrait settings - Draw the table —
autoTablelays out headers and body rows, wraps text, alternates row colors, and adds new pages as needed - Download —
doc.save('export.pdf')triggers a browser download of the finished PDF
CDN scripts (no bundler)
If you load Handsontable from a CDN, add jsPDF and the AutoTable plugin after Handsontable:
<script src="https://cdn.jsdelivr.net/npm/jspdf@3.0.4/dist/jspdf.umd.min.js"></script><script src="https://cdn.jsdelivr.net/npm/jspdf-autotable@5.0.7/dist/jspdf.plugin.autotable.min.js"></script>Then use the global jspdf.jsPDF constructor and call autoTable(doc, options) the same way as in the example (the plugin registers autoTable on the UMD build).
Alternative - html2canvas
You can capture the grid DOM with html2canvas and add the resulting image to a PDF with jsPDF. That path mirrors what users see on screen (merged cells, custom renderers, styling) but produces a raster snapshot — file size grows with resolution, text is not selectable, and accessibility is weaker than a table built from data.
What you learned
- How to read grid data with
hot.getData()and column headers withhot.getColHeader()to pass them to jsPDF AutoTable. - How AutoTable handles row wrapping, alternating row colors, and page breaks automatically so you can generate multi-page PDFs from a single call.
- How to use
doc.save('filename.pdf')to trigger a browser download of the finished PDF. - How to load jsPDF and AutoTable via CDN for environments without a bundler.
Next steps
- Explore Import from CSV or Excel to add the complementary import flow alongside your export feature.
- Explore the ExportFile plugin for built-in Excel export support without a third-party library.