This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/complete-guide-to-pdfjs.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. PDF.js tutorial: A complete guide with examples

Table of contents

    Build a browser PDF viewer with PDF.js. Then choose the layers you need for text selection, annotations, forms, and saving changes.
    PDF.js tutorial: A complete guide with examples
    TL;DR

    To render a PDF with PDF.js(opens in a new tab), import its display library, configure a matching worker, load the document, and await each page’s render task. The complete example below adds page navigation, zoom, and visible errors. For search, selectable text, forms, and annotation editing, use the bundled viewer or integrate its additional layers.

    What is PDF.js?

    PDF.js is Mozilla’s open source PDF renderer, licensed under Apache 2.0. You can build a custom viewer with its display API or host its complete viewer. A canvas example only draws the page; it doesn’t automatically include the viewer’s toolbar, text layer, or annotation editor.

    The project separates PDF parsing, the display API, and the viewer UI. The worker handles much of the parsing work, while the browser draws pages. Large documents still need careful memory management and testing on your target devices.

    Getting started with PDF.js

    Create a directory and install the version used in this example:

    Terminal window
    mkdir pdfjs-viewer
    cd pdfjs-viewer
    npm init -y
    npm install pdfjs-dist@6.3.289

    Save a PDF as document.pdf in this directory. You can use our sample PDF. Keep the library, worker, fonts, character maps, and WebAssembly resources from the same package version. The paths below serve them directly from node_modules for a local demonstration; copy the required resources into your public assets directory for deployment.

    Create the viewer page

    Save this as index.html. The controls stay disabled until the first page finishes rendering, and a download link remains available if rendering fails:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>PDF.js viewer</title>
    <style>
    body { margin: 1rem; font-family: sans-serif; }
    .toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    #viewport { overflow: auto; margin-top: 1rem; }
    canvas { display: block; }
    </style>
    </head>
    <body>
    <div class="toolbar" aria-label="PDF controls">
    <button id="previous" disabled>Previous page</button>
    <button id="next" disabled>Next page</button>
    <button id="zoom-out" disabled>Zoom out</button>
    <button id="zoom-in" disabled>Zoom in</button>
    <span id="page-number"></span>
    </div>
    <p id="status" role="status">Loading PDF…</p>
    <a href="document.pdf">Open or download the original PDF</a>
    <div id="viewport">
    <canvas id="pdf-canvas" aria-label="PDF page preview"></canvas>
    </div>
    <script type="module" src="index.js"></script>
    </body>
    </html>

    Load the document and render one page at a time

    Save this as index.js. It uses explicit module imports, waits for rendering to finish, and disables controls during a render. That prevents two render tasks from drawing onto the same canvas:

    import {
    getDocument,
    GlobalWorkerOptions,
    } from "./node_modules/pdfjs-dist/build/pdf.mjs";
    const assets = new URL("./node_modules/pdfjs-dist/", import.meta.url);
    GlobalWorkerOptions.workerSrc = new URL("build/pdf.worker.mjs", assets).href;
    const canvas = document.getElementById("pdf-canvas");
    const context = canvas.getContext("2d");
    const status = document.getElementById("status");
    const previous = document.getElementById("previous");
    const next = document.getElementById("next");
    const zoomOut = document.getElementById("zoom-out");
    const zoomIn = document.getElementById("zoom-in");
    let pdf;
    let pageNumber = 1;
    let scale = 1;
    let busy = false;
    function updateControls() {
    previous.disabled = busy || !pdf || pageNumber === 1;
    next.disabled = busy || !pdf || pageNumber === pdf.numPages;
    zoomOut.disabled = busy || !pdf || scale <= 0.5;
    zoomIn.disabled = busy || !pdf || scale >= 3;
    }
    async function renderPage() {
    if (busy || !pdf) return;
    busy = true;
    updateControls();
    status.textContent = "Rendering…";
    try {
    const page = await pdf.getPage(pageNumber);
    const viewport = page.getViewport({ scale });
    const outputScale = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.floor(viewport.width * outputScale);
    canvas.height = Math.floor(viewport.height * outputScale);
    canvas.style.width = `${viewport.width}px`;
    canvas.style.height = `${viewport.height}px`;
    await page.render({
    canvasContext: context,
    viewport,
    transform: [outputScale, 0, 0, outputScale, 0, 0],
    }).promise;
    document.getElementById("page-number").textContent =
    `Page ${pageNumber} of ${pdf.numPages}`;
    status.textContent = `Zoom ${Math.round(scale * 100)}%`;
    page.cleanup();
    } catch (error) {
    console.error(error);
    status.textContent = "Could not render this page. Try the original PDF link.";
    } finally {
    busy = false;
    updateControls();
    }
    }
    previous.addEventListener("click", () => {
    if (busy || !pdf || pageNumber <= 1) return;
    pageNumber -= 1;
    void renderPage();
    });
    next.addEventListener("click", () => {
    if (busy || !pdf || pageNumber >= pdf.numPages) return;
    pageNumber += 1;
    void renderPage();
    });
    zoomOut.addEventListener("click", () => {
    if (busy || !pdf) return;
    scale = Math.max(0.5, scale - 0.25);
    void renderPage();
    });
    zoomIn.addEventListener("click", () => {
    if (busy || !pdf) return;
    scale = Math.min(3, scale + 0.25);
    void renderPage();
    });
    try {
    pdf = await getDocument({
    url: "document.pdf",
    cMapUrl: new URL("cmaps/", assets).href,
    standardFontDataUrl: new URL("standard_fonts/", assets).href,
    wasmUrl: new URL("wasm/", assets).href,
    }).promise;
    await renderPage();
    } catch (error) {
    console.error(error);
    status.textContent = "Could not load the PDF. Check its URL and permissions.";
    }

    Serve the directory over HTTP rather than opening it through file://:

    Terminal window
    npx serve . -l 8080

    Open http://localhost:8080. Try the first and last pages, repeated navigation, both zoom limits, and a missing PDF. The scrollable preview prevents a wide PDF from widening the entire webpage on mobile.

    PDF.js page rendering example

    The existing recording illustrates navigation and zoom. Its appearance may differ from the controls above.

    0:00
    0:00

    Text selection, annotations, and forms

    Choose the integration level before adding features. Mozilla’s examples(opens in a new tab) and API reference(opens in a new tab) describe the display API; the complete viewer includes additional layers and UI behavior.

    RequirementWhat to use
    Draw a pageThe display API and a canvas, as above
    Extract textPDFPageProxy.getTextContent(); extraction doesn’t create a selectable text layer
    Select and copy text onscreenA text layer aligned with the page viewport, or the complete viewer
    Inspect annotationsPDFPageProxy.getAnnotations(); returned data isn’t a ready-made editing UI
    Fill supported forms or edit annotationsThe viewer’s form and annotation layers, with the relevant storage and editor integration
    Search and thumbnailsThe complete viewer or your own UI built around the APIs

    PDF coordinates and browser coordinates differ. Don’t place annotation rectangles directly on a canvas using raw PDF coordinates: Rotation, scale, and the different coordinate origins affect placement. Use the viewer’s annotation layer or the viewport’s conversion helpers when building overlays.

    A canvas has no selectable text or document reading order. Adding an accessible label to the preview doesn’t make the PDF accessible. Test keyboard navigation, focus, text layers, and assistive technology with representative documents.

    Can PDF.js save annotations and form changes?

    Yes. The bundled viewer can download supported annotation and form changes, and the display API exposes PDFDocumentProxy.saveDocument(). An editing UI can use HTML overlays and still serialize supported changes into PDF bytes.

    A custom integration must connect its edits to the document’s annotation storage before saving. Calling saveDocument() doesn’t invent annotations from unrelated DOM elements. Test exported files in other readers, especially if you rely on particular form fields or annotation types.

    PDF.js isn’t a general document-authoring SDK. Don’t equate a drawn signature with certificate-based digital signing or covering text with permanent redaction.

    Troubleshooting PDF.js

    SymptomWhat to check
    Worker or module fails to loadHTTP status, JavaScript MIME type, resource paths, and content security policy
    API and worker versions differDeploy both files from the same pdfjs-dist release; clear stale caches
    An external PDF failsCross-origin resource sharing (CORS), credentials, and the PDF server’s response
    Canvas-in-use errorAwait or cancel the previous render task before reusing its canvas
    Text can’t be selectedAdd a text layer or use the complete viewer
    Blurry pagesMatch canvas backing dimensions to device pixel ratio; cap resolution to control memory use
    Password-protected PDF won’t openAdd an appropriate password prompt; this minimal example reports the load failure

    Serve only the assets your application needs in production. For a single-page application, release loading tasks and documents when removing the viewer. Test the lifecycle of that integration separately from this static-page example.

    When to use PDF.js vs. Nutrient Web SDK

    PDF.js fits applications that can maintain their own integration or use Mozilla’s viewer. It supports more than passive viewing, including supported form filling and annotation editing.

    Nutrient Web SDK is a commercial alternative that ships these capabilities as product features rather than integration work:

    • Annotations — 20+ annotation types out of the box, including highlights, ink, stamps, notes, shapes, and measurement tools, with a built-in UI and XFDF/Instant syncing.
    • Forms — Form fields are interactive by default, and values are read and written through a direct API rather than by iterating page annotations.
    • Signatures and redaction — Digital signatures for cryptographic signing, and redaction that removes content from the file.
    • Performance — Progressive loading, render prioritization, and memory management are handled automatically, with no worker or range-request configuration for standard use cases.
    • Compliance and review workflows — Document comparison shows the differences between two versions; audit trails record views, annotations, and signatures; and the viewer targets WCAG 2.2 AA. An AI assistant can summarize, classify, and extract data from document content.
    • Support — Commercial technical support with service-level agreements. PDF.js has community support only.

    Optical character recognition (OCR), Office file rendering, and real-time collaboration are available through the relevant product components and configuration.

    For a feature comparison, see PDF.js vs. Nutrient. For a migration path from an existing PDF.js integration, see the migration guide. For a trial, use the demo or contact Sales.

    Start a free trial

    FAQ

    Is PDF.js free to use?

    Yes. PDF.js uses the Apache 2.0 license. Hosting, application integration, and maintenance remain your responsibility.

    Why does PDF.js need a worker?

    The worker handles PDF processing away from the main browser thread. Set its path explicitly and use the same version as the display library.

    Does rendering a PDF create selectable text?

    No. A canvas draws pixels. Use a text layer or the complete viewer for text selection and copying.

    Can PDF.js save edited PDFs?

    The viewer and save API support saving certain annotations and form edits. They don’t provide arbitrary editing of all existing PDF content. Validate your specific workflow and output files.

    Can PDF.js render Word or Excel files?

    PDF.js renders PDFs. Convert other file formats to PDF first, or use a viewer that supports the required formats.

    What is Nutrient Web SDK?

    Nutrient Web SDK is a commercial JavaScript library for viewing and editing documents in the browser. It provides annotations with 20+ types, form filling, digital signatures, and redaction through a documented API, with a built-in viewer UI.

    How do I migrate from PDF.js to Nutrient Web SDK?

    The migration guide covers viewer setup, search, annotations, forms, and thumbnails, with side-by-side code comparisons for each step.

    How do I get started with Nutrient Web SDK?

    Try the demo to evaluate it against your own documents, or contact Sales to discuss licensing and deployment.

    Hulya Masharipov

    Hulya Masharipov

    Technical Writer

    Hulya is a frontend web developer and technical writer who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

    Explore related topics

    Try for free Add a complete PDF viewer to your app