How to Generate PDFs in JavaScript in the Browser
Create a PDF in the browser with pdf-lib, preview it, and download it. Learn when to use PDF.js, handle common errors, and capture a web page as a PDF.
To create a PDF in the browser, use pdf-lib: create a document, add a page, draw content at coordinates, then save the document bytes. Use a Blob URL to download the result. Use PDF.js when you need to render or inspect an existing PDF; it is complementary to a PDF authoring library, not the creation path shown here.
1. Create and download a PDF
The following example is a complete browser page using the documented ES module approach. It creates a one-page PDF when the user clicks the button and downloads it. Save it as index.html and serve it from a local web server so the browser can load the module.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Create a PDF</title>
<button id="download">Create and download PDF</button>
<p id="status" role="status"></p>
<script type="module">
import { PDFDocument, StandardFonts, rgb } from 'https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/+esm';
const button = document.querySelector('#download');
const status = document.querySelector('#status');
button.addEventListener('click', async () => {
button.disabled = true;
status.textContent = 'Creating PDF…';
try {
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([595.28, 841.89]); // A4-sized page in PDF points
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
page.drawText('Hello from the browser', {
x: 48,
y: 780,
size: 22,
font,
color: rgb(0.12, 0.18, 0.28),
});
page.drawText('This PDF was created with pdf-lib.', {
x: 48,
y: 748,
size: 12,
font,
color: rgb(0.25, 0.29, 0.35),
});
const bytes = await pdfDoc.save();
const blob = new Blob([bytes], { type: 'application/pdf' });
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = 'example.pdf';
document.body.append(link);
link.click();
link.remove();
// Delay revocation so the browser can begin the download.
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
status.textContent = 'PDF downloaded.';
} catch (error) {
console.error(error);
status.textContent = 'PDF creation failed. See the console for details.';
} finally {
button.disabled = false;
}
});
</script>
</html>
The example pins the CDN version in its URL, as pdf-lib recommends for a production CDN include. Check the project’s current documentation before publishing or updating a pinned dependency. For an application build, install pdf-lib using your package manager and import it from your bundled application rather than relying on a runtime CDN.
What the code does
PDFDocument.create()creates a new document.addPage([width, height])creates a page. Dimensions are PDF points; 72 points equal one inch.drawText()places text at coordinates measured from the page’s bottom-left corner.save()asynchronously serializes the PDF into bytes.- A Blob URL and a temporary download link deliver the file through a user click. The URL is revoked afterward to release its browser-held resource.
2. Preview the generated PDF
For a basic preview, pdf-lib’s quick start shows saving a Base64 data URI and assigning it to an iframe. Add this after creating and drawing the document:
const preview = document.querySelector('#preview');
preview.src = await pdfDoc.saveAsBase64({ dataUri: true });
And add an iframe to the page:
<iframe id="preview" title="PDF preview" width="100%" height="600"></iframe>
A Base64 data URI is convenient for a small preview. For larger output, the Blob approach avoids turning the entire file into a Base64 string. The browser’s built-in PDF viewing behavior can vary; if you need a controlled viewer, use PDF.js to render pages and retrieve document information.
3. Choose the right PDF workflow
| Need | Approach | What to know |
|---|---|---|
| Create a document from scratch | pdf-lib | Create pages and draw text, shapes, images, or other content with explicit PDF coordinates. |
| Modify, fill, or combine PDFs | pdf-lib | Load existing bytes, then use its documented editing, form, and page-copying capabilities. |
| Display or inspect an existing PDF | PDF.js | Its display layer renders PDF pages and retrieves document information; it is not the authoring path in this guide. |
| Show a simple generated-file preview | pdf-lib data URI or Blob URL in an iframe | Choose based on output size and the viewer behavior your application needs. |
PDF authoring with coordinate-based drawing does not automatically reproduce a web page’s HTML and CSS. If your requirement is a designed document, lay out the PDF content deliberately or choose an HTML-to-PDF workflow that specifically supports your rendering requirements.
4. Add pages, content, and existing files
Multiple pages
Add a page for each section or chunk of content, and draw each page’s content within its own dimensions. For example:
const pageOne = pdfDoc.addPage([595.28, 841.89]);
pageOne.drawText('Page one', { x: 48, y: 780, size: 18, font });
const pageTwo = pdfDoc.addPage([595.28, 841.89]);
pageTwo.drawText('Page two', { x: 48, y: 780, size: 18, font });
For long text, calculate line wrapping and page breaks in your application. Drawing a long string does not make it flow automatically like a browser document. Keep text inside the page bounds and account for margins yourself.
Load and edit a PDF
When starting with a remote PDF, fetch its bytes and then load them. The server hosting the PDF must allow the browser’s request under its cross-origin policy, or your app must fetch it through a server you control.
const response = await fetch('/documents/source.pdf');
if (!response.ok) throw new Error(`Could not load PDF: ${response.status}`);
const sourceBytes = await response.arrayBuffer();
const pdfDoc = await PDFDocument.load(sourceBytes);
const pages = pdfDoc.getPages();
pages[0].drawText('Reviewed', { x: 48, y: 48, size: 10 });
const editedBytes = await pdfDoc.save();
For an existing PDF selected by the user, read the file in the browser with File.arrayBuffer() and pass those bytes to PDFDocument.load(). For a form, image embedding, or page copying, follow the relevant pdf-lib API examples and ensure any remote assets can be fetched by the browser.
Fonts and images
Standard fonts work for basic text. For a custom font, fetch or otherwise obtain the font bytes and embed them with pdf-lib’s font embedding support. Custom font embedding uses the library’s fontkit integration; follow its current setup documentation. Images can likewise be embedded from bytes, such as a fetched image response converted with arrayBuffer(). Test non-Latin glyphs, font licensing, transparency, and image dimensions with the actual assets used by your app.
5. Browser delivery and production considerations
- Run generation from a user action. A button click gives a clear download gesture and lets you display progress and errors.
- Keep large work responsive. PDF serialization and image/font embedding consume memory and CPU. Large source images and many pages can increase both; consider reducing image dimensions before embedding and splitting work when the product allows it.
- Release temporary URLs. Revoke Blob URLs after the browser has had time to start consuming them. Remove preview URLs when replacing or closing a document.
- Handle failures visibly. Check fetch responses, catch rejected save/load operations, and give users a retry path. Do not report success until the save operation and download initiation have completed.
- Validate remotely hosted inputs. Cross-origin policy, authentication, redirects, and unavailable assets can prevent browser fetches. Proxy through your own backend only when your product’s security model permits it.
- Protect sensitive content. Client-side generation keeps the document-generation work in the browser, but remote source files and assets still travel to their hosts. Avoid placing secrets in client code.
There is no universal performance figure for a PDF: output size and generation time depend on page count, fonts, images, and the target device. Measure representative documents in the browsers and devices your application supports. pdf-lib is a client-side dependency; budget for its download and for document bytes, embedded assets, and temporary copies in memory.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The module import fails | The page was opened as a local file, the CDN is unavailable, or the module URL is wrong. | Serve the page over HTTP during development, check the browser console and network panel, and verify the pinned package URL. For production, bundle a declared dependency. |
| Fetch reports a CORS error | The remote file or asset’s server does not allow the browser origin. | Configure the resource server’s CORS policy or fetch it through an application backend that is authorized to access it. |
| The PDF is blank or content is missing | Content may have been drawn outside the page, at an unexpected coordinate, or before required assets finished loading. | Remember that the origin is the page’s bottom-left; inspect the page dimensions and coordinates, and await asset fetches and embedding before saving. |
| Text is clipped or runs off the page | PDF drawing does not perform browser-style line wrapping or pagination for you. | Measure text, wrap it, and create a new page when content reaches the margin. |
| Some characters render incorrectly | The chosen font may not contain the needed glyphs. | Embed a font that supports the characters and verify fontkit setup for custom fonts. |
| Download does not start | The click handler may have failed before creating the Blob, or the browser may have blocked a delayed/non-user-initiated download. | Keep the download initiation in the button’s user action, inspect caught errors, and verify that the saved bytes are non-empty. |
| Memory use spikes | Large images, PDFs, or Base64 previews create large in-memory representations. | Resize images, prefer Blob URLs for larger previews, avoid retaining old bytes and URLs, and process fewer pages or assets at once. |
| An edited PDF fails to load | The file may be malformed, encrypted in a way the workflow cannot handle, or not actually a PDF. | Validate the file input and consult pdf-lib’s current load options and supported-document guidance; show a useful error rather than silently producing an empty output. |
7. Or skip the browser setup
If what you need is a PDF of a website page rather than a programmatically composed document, ScreenshotNeo can return a PDF from one GET request. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o page.pdf
The API base is https://api.screenshotneo.com/v1/shot. Use the documented PDF output option for your request. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. It can accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, no card required.
8. Frequently asked questions
Can I create a PDF without sending document contents to a server?
Yes. pdf-lib can create and serialize a PDF in a browser. Any remote fonts, images, or source PDFs your code fetches still make requests to their hosts.
Should I use pdf-lib or PDF.js?
Use pdf-lib for creating or modifying PDF bytes. Use PDF.js when the application needs to render or inspect an existing PDF.
Does pdf-lib convert my webpage’s CSS into a PDF?
The coordinate-based authoring workflow here does not convert ordinary DOM styling. Choose an HTML-to-PDF renderer if faithful page layout is a requirement, and verify its browser and CSS support separately.
Can the same code run in Node.js?
pdf-lib supports JavaScript environments beyond the browser, but browser download and preview code such as Blob URLs and iframe assignment is specific to the browser. Use a Node-appropriate file or response delivery method in a server application.


