Using Custom JavaScript in PDF Templates
Learn where JavaScript runs in PDF templates, how to add it safely, and how to troubleshoot rendering, data, libraries, and PDF-reader issues.

Short answer: JavaScript in a PDF template can run in different places. It may execute while an HTML template is rendered into a PDF, inside a visual template builder, or later inside a PDF viewer such as Acrobat. These are separate runtimes with different APIs, permissions, timing, and failure modes.
For an HTML-to-PDF service such as PDFMonkey, add <script> tags to the HTML template and enable the service’s JavaScript or script-injection setting. For a visual builder, use its global Custom JS editor or an HTML block’s script option. For a finished PDF, use a PDF scripting API such as Acrobat JavaScript or PDF-LIB. Code written for one stage should not be assumed to work in another.
1. Identify the JavaScript execution context
Before writing code, answer one question: when should this JavaScript run?
| Context | What JavaScript can change | Typical use | Main constraint |
|---|---|---|---|
| HTML template renderer | DOM, styles, generated charts, calculated display values | Prepare HTML before PDF pagination | Renderer timing and browser support |
| Visual template builder | Bindings, block content, shared formatting helpers | Reusable functions without editing all markup | Builder settings and block execution rules |
| PDF viewer or PDF API | Actions and document objects in an already-created PDF | Interactive forms, document actions, viewer automation | Reader permissions and viewer compatibility |
PDFMonkey’s Code Templates documentation answers the common question directly: script tags can be added in the HTML tab. Its Builder product has a separate Custom JS editor, HTML-block scripts, and an External Assets manager. Adobe documents Acrobat JavaScript as an API for creating and modifying PDF documents, while PDF-LIB documents adding JavaScript that runs when a PDF is opened. Those APIs are not interchangeable.
2. Add JavaScript to a PDFMonkey Code Template
In a PDFMonkey Code Template, place your script in the HTML tab. Turn on JavaScript injection in the template settings before expecting the input payload to be available as $docPayload.
Minimal runnable template
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; }
.total { font-size: 24px; font-weight: 700; }
</style>
</head>
<body>
<h1 id="customer-name"></h1>
<p class="total" id="invoice-total"></p>
<script>
const payload = $docPayload || {};
const customer = payload.customer || {};
const total = Number(payload.total || 0);
document.getElementById('customer-name').textContent =
customer.name || 'Customer';
document.getElementById('invoice-total').textContent =
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: payload.currency || 'USD'
}).format(total);
</script>
</body>
</html>
The script changes the DOM before the renderer creates the PDF. Prefer textContent for data supplied by users; it prevents a payload value from being interpreted as HTML. Guard optional objects and convert numeric values explicitly so missing fields do not become NaN or throw an exception.
Liquid runs before JavaScript
PDFMonkey evaluates Liquid interpolation before JavaScript. A value calculated in JavaScript cannot then be inserted with Liquid syntax. If Liquid can perform the formatting, do it in Liquid first; use JavaScript when you need DOM operations, a browser API, or a library.
3. Use the PDFMonkey Builder workflow
Builder templates provide three places for code:
- Global Custom JS: put shared formatting and utility functions here.
- HTML block scripts: put logic that belongs to one block here, then enable the setting that runs scripts in the editor.
- External Assets: add third-party JavaScript or CSS libraries through the asset manager.
If a binding or HTML block must call a global function, attach it to window. A function declared only with const may not be visible to the builder’s binding environment.
// Global Custom JS
window.formatCurrency = function (value, currency) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency || 'USD'
}).format(Number(value || 0));
};
window.formatDate = function (value) {
return new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeZone: 'UTC'
}).format(new Date(value));
};
Keep global code focused on formatting and small helpers. Heavy computation and broad DOM manipulation can slow generation and make block order difficult to reason about. For block-specific work, use the HTML block’s script area and verify that “Run scripts in editor” is enabled.
4. Load libraries, charts, and external assets
External code has operational requirements. PDFMonkey’s Code Template documentation says URL-loaded JavaScript requires a paid account and that documents fail on a free account when this requirement is not met. Builder external assets require an internet connection at generation time. A library that works in a local browser can therefore fail in a hosted render.
Pin library versions where possible, use HTTPS URLs, and test generation with the same network conditions used in production. For charts, disable animation because a PDF has no animation timeline. PDFMonkey’s Chart.js example also disables responsive sizing to avoid layout surprises:
const canvas = document.getElementById('sales-chart');
new Chart(canvas, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'Sales',
data: [12, 19, 14],
backgroundColor: '#2563eb'
}]
},
options: {
animation: false,
responsive: false,
maintainAspectRatio: false
}
});
PDFMonkey describes Chart.js as its most reliable chart option. It also documents a known ApexCharts border issue in some PDF readers, especially Windows and Android readers. Treat those as vendor-specific observations and inspect the final PDF on the readers your customers use.
5. Dates, time zones, and deterministic output
A server’s time zone may differ from the time zone you intend to display. Use Liquid time-zone and date filters for common formatting. If JavaScript is necessary, pass an explicit locale and time zone to toLocaleString:
const invoiceDate = new Date(payload.issued_at);
const label = invoiceDate.toLocaleString('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Europe/London'
});
document.querySelector('#issued-at').textContent = label;
Always provide an explicit time zone for invoices, statements, and other records where reproducibility matters. Avoid relying on the renderer’s local clock or default locale.
6. Embedded PDF JavaScript is a different feature
HTML-template JavaScript changes the page before PDF creation. Embedded PDF JavaScript is stored in the resulting file and may run when a compatible viewer opens it. Adobe’s Acrobat JavaScript documentation covers APIs for creating and modifying PDF documents. PDF-LIB exposes an API for adding document JavaScript that executes on opening.
Use embedded scripting only when the target readers support it and your security model permits it. Many browser PDF viewers ignore or restrict document JavaScript. A calculation that must appear in every reader should be rendered into the page during PDF generation instead.
import { PDFDocument } from 'pdf-lib';
import fs from 'node:fs/promises';
const input = await fs.readFile('invoice.pdf');
const pdfDoc = await PDFDocument.load(input);
pdfDoc.addJavaScript('welcome', 'app.alert("Document opened");');
await fs.writeFile('invoice-with-script.pdf', await pdfDoc.save());
This code modifies an existing PDF; it does not execute JavaScript during an HTML template render.
7. A practical decision checklist
- Choose the stage: HTML render, visual builder, or finished PDF.
- Confirm how input data is exposed and whether injection must be enabled.
- Decide whether Liquid can perform the transformation before using JavaScript.
- Make layout dimensions explicit for charts, canvases, and long text.
- Pin external assets and confirm plan and network requirements.
- Disable animation and avoid timing-dependent effects.
- Generate a PDF and inspect it in the viewers your users need.
- Keep a fixture payload so changes can be compared deterministically.
8. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
$docPayload is undefined |
JavaScript injection is disabled | Enable injection in template settings and regenerate. |
| Script appears as text | Tags were escaped or placed in a text field | Put the code in the HTML tab or an HTML block. |
| Liquid shows an old value | Liquid ran before JavaScript | Compute or format the value with Liquid, or update the DOM directly. |
| External library fails | Free-plan restriction, no generation-time network, bad URL, or blocked CDN | Check the plan, connection, HTTPS URL, and asset availability; consider bundling supported code. |
| Chart is blank | Animation or responsive sizing depends on a live browser viewport | Set animation: false, responsive: false, and explicit dimensions. |
| Function cannot be called by a binding | Helper is not global | Assign it to window. |
| Date differs between environments | Implicit locale or server time zone | Pass locale and timeZone explicitly. |
| Browser preview works but PDF is wrong | Preview and final pagination/rendering differ | Use the Debug preview to inspect console errors, then validate the generated PDF itself. |
| Black chart borders in some readers | Reader-specific ApexCharts rendering issue | Test with Chart.js or inspect output on affected readers. |
9. Performance, reliability, and cost considerations
Every script adds work to the render. Prefer one pass over the DOM, cache repeated calculations, and avoid polling loops. Large libraries and image-heavy charts increase generation time and memory use. Keep external requests to a minimum because a transient CDN or network failure can fail the document.
For reliable output, make scripts idempotent: running them twice should produce the same DOM. Wait for the data your script needs rather than using arbitrary delays, and set explicit element sizes so pagination does not change when a font or image loads late. Keep a small set of representative payloads: empty data, long names, large totals, missing optional fields, and the largest expected line-item list.
Cost depends on the PDF service’s plan and the features you enable. External-asset requirements can also change which plan is suitable. Do not assume that a browser-only dependency is available in every tier or renderer; confirm the provider’s current documentation before committing to it.
10. Or skip the browser setup
If your goal is a clean capture of an HTML template, ScreenshotNeo can return a screenshot or PDF with one GET request. It accepts PNG, JPEG, WebP, and PDF output and offers controls for full-page capture, waiting, custom CSS and JavaScript, headers, cookies, user agents, blocking, device presets, and PDF paper settings. Cookie banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the verdict and billing status in headers.

See the ScreenshotNeo API documentation for the complete option list.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
11. FAQ
Can JavaScript create a value that Liquid uses later?
No. Liquid interpolation happens before JavaScript in the documented Code Template flow. Format with Liquid first or write the result into the DOM from JavaScript.
Should I use HTML JavaScript or embedded PDF JavaScript?
Use HTML JavaScript for deterministic content and layout during generation. Use embedded PDF JavaScript only for viewer-dependent interactions in readers that support it.
Why does a script work locally but fail in generation?
The hosted renderer may have different browser APIs, network access, plan restrictions, timing, or security rules. Check external assets, settings, and the generated PDF.
Can I rely on PDF viewer JavaScript in a browser?
No. Browser PDF viewers vary widely and often restrict document scripts. Render essential values into the PDF itself.
What is the safest way to handle user data?
Use textContent, validate types, escape any HTML you intentionally generate, and avoid evaluating payload strings as code.


