Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Mozilla PDF.js when you need an in-browser PDF preview you can control. Install the pdfjs-dist package, load a PDF through PDF.js, render pages into canvas elements, and add the controls your application needs. If you need a ready-made toolbar, start with PDF.js’s viewer layer instead of building every control yourself.

This guide covers both approaches, loading PDFs from URLs or binary data, page and zoom controls, cross-origin requirements, customization, troubleshooting, and a browser-free screenshot option.

Choose the PDF.js layer that matches your UI

PDF.js is an HTML5 PDF viewer project supported by Mozilla. Its distribution on npm is named pdfjs-dist. The project is divided into layers:

  • Core: parses PDF files and performs low-level work.
  • Display: exposes the JavaScript API you use to load documents, fetch pages, and render them.
  • Viewer: the complete user interface with page navigation, zoom, search, thumbnails, and other controls.

Use the full viewer when you want a proven interface quickly. Use the display layer when your product needs a tailored toolbar, custom layout, application state, or a single-page preview. Mozilla asks developers embedding the viewer not to ship an unmodified copy; treat it as a starting point and adapt the UI.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Install PDF.js in a JavaScript project

Create a project and install the package:

npm install pdfjs-dist

PDF.js includes a worker file. Your bundler must know where that worker is, or document loading will fail with worker-related errors. The exact import path depends on your bundler and the version you install, so expose the worker from the package according to that bundler’s asset rules.

Build a minimal custom preview

The following example uses the display layer. It loads a PDF URL, renders the first page, and provides previous, next, and zoom controls. It assumes your bundler can import the package’s modern build and worker asset.

import * as pdfjsLib from 'pdfjs-dist/build/pdf.mjs';
import pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;

const canvas = document.querySelector('#pdf-canvas');
const context = canvas.getContext('2d');
const pageNumberLabel = document.querySelector('#page-number');
const previousButton = document.querySelector('#previous');
const nextButton = document.querySelector('#next');
const zoomInButton = document.querySelector('#zoom-in');
const zoomOutButton = document.querySelector('#zoom-out');

let pdfDocument;
let pageNumber = 1;
let scale = 1.25;

async function renderPage(number) {
  const page = await pdfDocument.getPage(number);
  const viewport = page.getViewport({ scale });
  canvas.width = viewport.width;
  canvas.height = viewport.height;
  await page.render({ canvasContext: context, viewport }).promise;
  pageNumberLabel.textContent = `${number} / ${pdfDocument.numPages}`;
  previousButton.disabled = number <= 1;
  nextButton.disabled = number >= pdfDocument.numPages;
}

async function openPdf(source) {
  pdfDocument = await pdfjsLib.getDocument(source).promise;
  pageNumber = 1;
  await renderPage(pageNumber);
}

previousButton.addEventListener('click', async () => {
  if (pageNumber > 1) await renderPage(--pageNumber);
});
nextButton.addEventListener('click', async () => {
  if (pageNumber < pdfDocument.numPages) await renderPage(++pageNumber);
});
zoomInButton.addEventListener('click', async () => {
  scale = Math.min(scale + 0.2, 4);
  await renderPage(pageNumber);
});
zoomOutButton.addEventListener('click', async () => {
  scale = Math.max(scale - 0.2, 0.5);
  await renderPage(pageNumber);
});

openPdf('/documents/handbook.pdf').catch(console.error);

Pair it with simple markup:

<div class="pdf-toolbar">
  <button id="previous">Previous</button>
  <span id="page-number">Loading…</span>
  <button id="next">Next</button>
  <button id="zoom-out">−</button>
  <button id="zoom-in">+</button>
</div>
<canvas id="pdf-canvas" aria-label="PDF page preview"></canvas>

For a multi-page preview, create one canvas per visible page or render pages into a virtualized list. Rendering every page immediately uses more memory; render the current page and nearby pages as the user scrolls.

Load a PDF from a URL or binary data

Loading by URL

Pass a URL string to getDocument, as in the example above. The PDF must be served in a way the browser can fetch. A URL on another origin requires the server to permit the requesting origin with appropriate CORS headers. Authentication headers, cookies, redirects, and range requests must also be allowed by that server.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Loading an ArrayBuffer or Uint8Array

If your application already downloaded the file, pass binary data instead of a URL:

const response = await fetch('/api/documents/42', {
  credentials: 'include'
});
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
const pdf = await pdfjsLib.getDocument({ data: bytes }).promise;
await renderPageFrom(pdf, 1);

This approach lets your application control authentication and download progress. Do not expose a private document URL in the page if the browser should receive the file only after an authorization check.

Use the ready-made PDF.js viewer

The viewer application is useful when you need navigation, thumbnails, search, download, and zoom without implementing each feature. Deploy the viewer files with your application and open its viewer page with a PDF URL:

/pdfjs/web/viewer.html?file=%2Fdocuments%2Fhandbook.pdf

The file value must be URL-encoded. The viewer documentation also describes URL controls for opening at a page, selecting a zoom level, choosing a named destination, and setting sidebar mode. A typical link can therefore look like:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
/pdfjs/web/viewer.html?file=%2Fdocuments%2Fhandbook.pdf#page=4&zoom=page-width

Viewer URL options are convenient for links and bookmarks, while the application API is better when your own interface needs to change the document or synchronize state. Test the options against the PDF.js version you deploy; viewer option documentation has changed over time.

Customize rendering and interaction

Responsive sizing

Compute a scale from the available container width rather than fixing one value. Obtain the page at scale 1, calculate the ratio between the container width and the viewport width, then render at that ratio. Re-render on a debounced resize event so a window drag does not start dozens of expensive paints.

High-density displays

For crisp text on a retina display, multiply the canvas backing dimensions by devicePixelRatio while keeping its CSS dimensions equal to the logical viewport. This increases memory use, so cap the ratio for very large pages or mobile devices.

Text selection and accessibility

A canvas alone is an image and does not expose selectable text. The PDF.js viewer adds text and annotation layers. If you build a custom interface, add those layers when users need search, selection, links, or accessible reading order, and keep keyboard focus visible on your controls.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Large documents

Render only pages near the viewport, cancel work for pages that have scrolled away, and remove canvases that are no longer needed. Display a loading state while getDocument resolves and handle password-protected or malformed files as recoverable errors rather than leaving a blank panel.

Cross-origin, server, and security requirements

  • Serve the PDF with a correct PDF content type and a response that the browser can read.
  • For a different origin, configure CORS for the exact application origin. If credentials are used, the server cannot use a wildcard origin.
  • Ensure redirects preserve authorization and CORS headers.
  • Do not insert untrusted PDF-derived HTML into your page. Keep PDF.js assets and worker files under your control and update them through your normal dependency process.
  • Apply your own access control before issuing a document URL or returning binary data.

Same-origin restrictions are a browser rule, not a PDF.js setting. When a remote URL fails in the viewer, inspect the browser network panel and the server’s CORS response before changing rendering code.

Common errors and fixes

“Setting up fake worker” or worker loading errors

The worker URL is missing, points to a development-only path, or is blocked by your content-security policy. Import or copy the worker from pdfjs-dist, set GlobalWorkerOptions.workerSrc, and permit that script in your policy.

Failed to fetch or CORS errors

The browser cannot read the PDF response. Host the file on the same origin, add a CORS rule for the application origin, or fetch the file through your authenticated server and pass a Uint8Array.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The viewer is blank but the request succeeds

Check the console for a worker-version mismatch, an invalid PDF, or a blocked worker. Confirm that the viewer and worker came from the same PDF.js package version.

Only part of a document renders

Verify that the response is not truncated and that any range-request or compression configuration is compatible with your server. Try downloading the complete file and passing binary data to separate transport problems from rendering problems.

Pages are blurry or consume too much memory

Adjust scale and device-pixel-ratio handling. Avoid rendering every page at maximum resolution; virtualize the page list and release canvases that are outside the visible region.

Performance and reliability checklist

  • Lazy-render pages and cancel obsolete render tasks.
  • Debounce resize and zoom operations.
  • Show progress, retry transient downloads, and provide a download fallback.
  • Set realistic limits for file size and page count on your server.
  • Test PDFs with embedded fonts, images, annotations, encryption, very large pages, and malformed structure.
  • Pin and update pdfjs-dist deliberately; keep the viewer and worker versions aligned.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Commercial embedded alternative

PDF.js Express advertises a free in-browser viewer and a commercial Plus offering for embedding in JavaScript applications. The available material does not establish its current pricing, license terms, feature limits, or browser coverage, so verify those details directly before selecting it. For maximum control and a known open-source foundation, PDF.js display and viewer layers remain the straightforward starting point.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your goal is a static image or PDF of a web page rather than an interactive document reader, ScreenshotNeo provides a website screenshot API. It accepts one request and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());

See the ScreenshotNeo documentation for options. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I preview a PDF without showing a canvas?

Yes. Use the PDF.js viewer layer, which adds text, annotation, and navigation layers, or build those layers around the display API. A canvas by itself is only the painted page.

Does PDF.js automatically bypass CORS?

No. The PDF URL still has to satisfy browser origin rules. Configure the server or fetch the bytes through an authorized same-origin endpoint.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I use the viewer or display API?

Choose the viewer for a complete interface you can adapt; choose the display API when your application needs its own controls, layout, and state model.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.