api reference
PDF API Reference
exportPdf and downloadPdf: the grid, the calendar, the Kanban board and the Gantt as paginated PDFs with real, selectable text, page size, orientation, headers, footers and embedded fonts.
API reference › The PDF module
All 30 pages Everything on one page → Developer guide →
The PDF export
exportPdf(view, options) and downloadPdf(view, options) (modules/pdf, opt-in) turn a grid's current view, a calendar, a Kanban board or a Gantt (below) into a paginated PDF whose text is real, selectable text. pdf-lib stays on your page: load its browser build (it leaves PDFLib on the window) or pass its namespace as pdfLib; the module bundles none of it, and because pdf-lib needs no DOM the same call runs in Node.
<script src="https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
import { exportPdf, downloadPdf } from '@toclocoinc/lattice-grid/modules/pdf';
const blob = await exportPdf(grid, { pageSize: 'A4', orientation: 'landscape', title: 'Sales by region' });
await downloadPdf(grid, { rows: 'selected', fileName: 'selected-sales' }); // saved as selected-sales.pdf
grid.export.pdf({ download: true }); // beside grid.export.excel
What prints. What the screen shows: the visible columns in screen order with their widths, scaled to fill the page width (fit: 'width', the default) or kept and tiled across as many pages as the table needs (fit: 'split', the start-pinned columns leading every tile); group rows with their totals; pinned-top rows first and pinned-bottom rows, the grand total among them, last; start-pinned columns first and end-pinned columns last; every cell's text exactly as its formatter put it on screen. A column the grid is redacting prints its header and, in each non-empty cell, a fixed six-bullet mask, so neither the value nor its length reaches the file. Control columns (the selection checkbox, the expander) are left out, and so is a column with export: { pdf: false }. rows: 'selected' prints only the selected data rows, in display order. The column headers repeat on every page, and every row is one line high, so a page break never splits a row; text too wide for its column ends in an ellipsis.
Page furniture. pageSize is 'A4', 'Letter', 'A3' or 'Legal' (anything else is refused by name) and orientation 'portrait' or 'landscape'. title prints at the top of the first page and is the document's title; header and footer print on every page, each a string or ({ page, pages }) => text; pageNumbers prints 3 / 12 bottom right unless false, or your own wording from a function. theme: true fills the header and the zebra stripes with the live theme tokens read off the grid's element (--lattice-surface, --lattice-header-foreground, --lattice-row-stripe, --lattice-surface-alt, --lattice-foreground, --lattice-border-color); without it the table is plain black on white. fileName names the file downloadPdf saves (.pdf added when missing, export.pdf by default); in Node, where there is no document, downloadPdf returns the PDF and saves nothing.
Fonts. Text is set in Helvetica by default, which covers Western European text. fonts: [{ family, url }] embeds TTF/OTF fonts (subset to the glyphs used; data, the font's bytes, may stand in for url) and needs pdf-lib's fontkit build, the fontkit global or fontkit: a string is set in the first listed font that has every one of its characters, and in Helvetica otherwise, so a Noto Sans Arabic and a Noto Sans JP passed together set Arabic and Japanese cells while the Latin text stays in Helvetica. Joining forms and right-to-left order are fontkit's shaping.
Where it shows up. While the module is loaded, a grid built by createGrid carries grid.export.pdf(options) (with download: true it downloads), the tool-panel rail carries a PDF button ('pdf') beside Excel, and the context menu's export block offers Export visible rows to PDF and Export selected rows to PDF. Every framework adapter hands you the grid, so each exposes the PDF export exactly as it exposes the Excel one. The labels' keys are toolbar.exportPdf, menu.exportPdfVisible and menu.exportPdfSelected; the module carries their English.
A grouped grid to a Letter landscape PDF in Node, the same through grid.export.pdf, and downloadPdf
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { exportPdf, downloadPdf } = await import('../packages/modules/pdf/index.js');
// On a page pdf-lib's browser build leaves PDFLib on the window. In Node it is
// handed in: here, the repository's test copy of pdf-lib's own ESM build.
const PDFLib = await import('../test/fixtures/pdf-lib/pdf-lib.esm.min.js');
const grid = createGrid(createTestDom().root, {
rowKey: 'id',
columns: [{ field: 'region' }, { field: 'amount', type: 'number', total: 'sum' }],
rows: Array.from({ length: 120 }, (_, i) => ({ id: 'r' + i, region: i % 2 ? 'North' : 'South', amount: i })),
});
grid.columns.group(['region']);
const blob = await exportPdf(grid, {
pdfLib: PDFLib, pageSize: 'Letter', orientation: 'landscape', fit: 'width', rows: 'visible',
title: 'Sales', header: 'Q3 report', footer: ({ page }) => 'Confidential, page ' + page,
pageNumbers: true, theme: false, fonts: [], fontkit: null,
});
const doc = await PDFLib.PDFDocument.load(new Uint8Array(await blob.arrayBuffer()));
const { width, height } = doc.getPage(0).getSize();
const viaGrid = await grid.export.pdf({ pdfLib: PDFLib, fit: 'split' }); // beside grid.export.excel
const saved = await downloadPdf(grid, { pdfLib: PDFLib, fileName: 'sales' }); // Node: returned, not saved
return [blob.type, doc.getPageCount(), `${width}x${height}`, doc.getTitle(),
viaGrid.type === 'application/pdf', saved.size > 0].join(' | '); // application/pdf | 4 | 792x612 | Sales | true | true
The calendar, the Kanban board and the Gantt
The same two calls take a calendar (createCalendar), a Kanban board (createKanban) or a Gantt (createGantt); which view it is comes from the instance, never from an option. Every option above applies except rows: 'selected', which is the grid's and is refused by name for the others. Each view carries export.pdf(options) while the module is loaded (with download: true it saves), and every framework adapter hands you the instance, so it is reached there the same way.
import { downloadPdf } from '@toclocoinc/lattice-grid/modules/pdf';
await downloadPdf(calendar, { fileName: 'november' }); // the month, week or day on screen
await downloadPdf(board, { fit: 'split' }); // columns at their screen width, tiled
await gantt.export.pdf({ orientation: 'landscape', download: true }); // table + timeline, tiled both ways
Calendar. The current view as laid out on screen: the month or week as rows of day cells under the weekday header, every event one line with its time, its title and the colour its edge takes (a multi-day span as one bar across its days); the week with times and the day as an hour axis beside one column per day, the all-day row first, each event a box at its time in its colour. The title defaults to the header's own (November 2026). Every event of a day prints, the +N more overflow included. An event is never split: a week row that does not fit moves whole to the next page, a row taller than a page continues on the next with the weekday header and its day numbers repeated, and in the time grid a long day continues on the next page with the day headers repeated, the page break always placed between events. The header shows a PDF button beside the view buttons; its label is calendar.exportPdf.
Kanban. The columns side by side in screen order, each headed by its title and its card count (5/9 against a WIP limit), and each column's cards as the current filters leave them: title, subtitle, due date, badges, labels, points, the checklist count and progress. Covers, attachments and avatars are pictures and are not printed. fit: 'width' (the default) shares the page width between the columns; fit: 'split' keeps each column's on-screen width and tiles the columns across pages. A long column continues on the next page under its own header, every column header repeats on every page, and a card is never split. Every column's header menu carries Export board to PDF (kanban.exportPdf) after the host's own items.
Gantt. The task table beside the timeline, as the mounted split view last painted them: its rows in screen order with their outline depth, its columns, widths and cell text, its day width. The timeline keeps that day width and is cut into date tiles (fit: 'split', the Gantt's default), so the PDF is paginated along both axes; fit: 'width' scales the whole plan onto the page width instead. The table, its headers and the time scale (months over days, weeks or months, by how wide a day is) repeat on every page. Bars, progress, milestones, summary bars, dependency links, the today line and the weekend shading are vector paths and fills, never a bitmap, and every row is one line high, so a bar row is never split. With no split view mounted the schedule prints in its own order with a name, start and end column. The split view's header carries a PDF button beside the zoom control (gantt.exportPdf). On the builder's machine a Gantt of 500 tasks over one year exported in 832 ms and 1012 ms in two runs (342 landscape pages).
A calendar's month, then its day, in Node
const { createCalendar } = await import('../packages/modules/calendar/index.js');
const { exportPdf } = await import('../packages/modules/pdf/index.js');
const PDFLib = await import('../test/fixtures/pdf-lib/pdf-lib.esm.min.js');
const calendar = createCalendar(null, {
startProperty: 'start', view: 'month', date: '2026-11-08', locale: 'en-GB', colorProperty: 'colour',
rows: [
{ id: 1, title: 'Planning', start: '2026-11-03T09:30', colour: '#2563eb' },
{ id: 2, title: 'Release', start: '2026-11-19T16:00', colour: '#e11d48' },
],
});
const blob = await calendar.export.pdf({ pdfLib: PDFLib }); // the month on screen
const doc = await PDFLib.PDFDocument.load(new Uint8Array(await blob.arrayBuffer()));
calendar.setView('day');
calendar.goTo('2026-11-03');
const day = await exportPdf(calendar, { pdfLib: PDFLib, orientation: 'landscape' });
return [blob.type, doc.getPageCount(), doc.getTitle(), day.size > 0].join(' | '); // application/pdf | 1 | November 2026 | true
A board fitted to the page width, then tiled at its columns' screen width
const { createKanban } = await import('../packages/modules/kanban/index.js');
const { exportPdf } = await import('../packages/modules/pdf/index.js');
const PDFLib = await import('../test/fixtures/pdf-lib/pdf-lib.esm.min.js');
const statuses = ['todo', 'doing', 'review', 'done', 'archived'];
const board = createKanban(null, {
columnProperty: 'status',
columns: statuses.map((id) => ({ id, title: id, wipLimit: id === 'doing' ? 3 : undefined })),
card: { title: 'title', subtitle: 'owner', due: 'due', badges: 'size' },
rows: Array.from({ length: 40 }, (_, i) => ({
id: i, status: statuses[i % 5], title: 'Card ' + i, owner: 'Ann', due: '2026-12-01', size: 'M',
})),
});
const fitted = await board.export.pdf({ pdfLib: PDFLib }); // columns share the page width
const tiled = await exportPdf(board, { pdfLib: PDFLib, fit: 'split' }); // columns at their screen width
const pages = async (b) => (await PDFLib.PDFDocument.load(new Uint8Array(await b.arrayBuffer()))).getPageCount();
return [await pages(fitted), await pages(tiled)].join(' | '); // 1 | 3
A Gantt tiled along both axes, then scaled to one page width
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { exportPdf } = await import('../packages/modules/pdf/index.js');
const PDFLib = await import('../test/fixtures/pdf-lib/pdf-lib.esm.min.js');
const gantt = createGantt({
title: 'Roll-out',
tasks: Array.from({ length: 60 }, (_, i) => ({ id: 't' + i, name: 'Task ' + i, start: '2026-01-05', duration: 3 + i })),
dependencies: [{ from: 't0', to: 't1', type: 'FS' }],
});
const tiled = await gantt.export.pdf({ pdfLib: PDFLib, orientation: 'landscape' }); // both axes
const fitted = await exportPdf(gantt, { pdfLib: PDFLib, fit: 'width' }); // one page width
const read = async (b) => PDFLib.PDFDocument.load(new Uint8Array(await b.arrayBuffer()));
const a = await read(tiled);
const b = await read(fitted);
return [a.getTitle(), a.getPageCount(), b.getPageCount()].join(' | '); // Roll-out | 3 | 2
Type reference
Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.
The PDF export
PdfExportOptions
What the opt-in PDF export takes (`modules/pdf`,: `exportPdf(grid, options)`, `downloadPdf(grid, options)` and, while the module is loaded, `grid.export.pdf(options)`. The PDF is the grid's current view: its visible columns in screen order and widths, group rows with their totals, pinned rows and columns in place, each cell's text as formatted on screen and a redacted column's cells as a fixed mask. The column headers repeat on every page and a row is never split across pages. Text is real, selectable text in an embedded font.
| Property | Type | Description |
|---|---|---|
| pageSize | 'A4' | 'Letter' | 'A3' | 'Legal' | The page size: `'A4'` (the default), `'Letter'`, `'A3'` or `'Legal'`. Any other value is refused by name. (optional) |
| orientation | 'portrait' | 'landscape' | `'portrait'` (the default) or `'landscape'`. (optional) |
| fit | 'width' | 'split' | How a view wider than the page fits: `'width'` scales it so it fills the page width; `'split'` keeps its on-screen widths and tiles it across as many pages as it needs. A grid's columns (the start-pinned ones leading every tile) and a board's columns default to `'width'`; a Gantt's timeline defaults to `'split'`, cut into date tiles at the screen's day width, and `'width'` scales the whole plan onto the page width. (optional) |
| rows | 'visible' | 'all' | 'selected' | Which rows of a grid: `'visible'` (the default) prints the view, pinned rows and group rows included; `'selected'` only the selected data rows, in display order. `'all'` is the view too: a PDF is a picture of the grid. Grid-only: a calendar, a board or a Gantt prints its whole view and refuses `'selected'` by name. (optional) |
| title | string | A title printed at the top of the first page (of each column tile, on a grid), and the document's title. A calendar defaults to its header's title (`November 2026`), a Gantt to `createGantt({ title })`. (optional) |
| header | string | ((at: { page: number; pages: number }) => string) | A running header on every page: text, or `({ page, pages }) => text`. (optional) |
| footer | string | ((at: { page: number; pages: number }) => string) | A running footer on every page, bottom left: text, or `({ page, pages }) => text`. (optional) |
| pageNumbers | boolean | ((at: { page: number; pages: number }) => string) | Page numbers, bottom right: `true` (the default) prints `3 / 12`, `false` none, a function `({ page, pages }) => text` its own wording. (optional) |
| fileName | string | The file `downloadPdf` saves, `.pdf` added when missing. `'export.pdf'` by default. (optional) |
| theme | boolean | Colour the header row and the zebra stripes from the live theme tokens (`--lattice-surface`, `--lattice-header-foreground`, `--lattice-row-stripe`, `--lattice-surface-alt` for group rows, `--lattice-foreground`, `--lattice-border-color`), read off the grid's element. Off by default: a plain black-on-white table. (optional) |
| fonts | Array<{ family: string; url?: string; data?: ArrayBuffer | Uint8Array }> | TTF/OTF fonts to embed (subset to the glyphs used), in preference order: a string is set in the first font that has every one of its characters, and in Helvetica, the default, otherwise. `data` (the font's bytes) may stand in for `url`. Needs `fontkit`. (optional) |
| pdfLib | unknown | The page's pdf-lib namespace. The `PDFLib` global its browser build leaves behind by default; the module bundles none. (optional) |
| fontkit | unknown | pdf-lib's fontkit, needed only for `fonts`. The `fontkit` global by default. (optional) |