Skip to content

Latest commit

 

History

History
476 lines (341 loc) · 19.9 KB

index.md

File metadata and controls

476 lines (341 loc) · 19.9 KB
layout pdfjs-viewer.njk
title pdfjs-viewer · PDF viewer web component
description Docs for @missing-elements/pdfjs-viewer, a self-hostable PDF viewer web component based on the PDF.js default viewer. Install, attributes, API, theming, hosting and framework guides.

The full PDF.js viewer as one HTML element. It runs in an isolated iframe and has no runtime dependencies. PDF.js, its worker and the data some PDFs need ship inside the package, so by default nothing loads from a CDN. The one exception is locale files, which come from jsDelivr when you set locale. Each Try button applies its example to the live preview, and Reset puts the preview back to its defaults.

Quick start

Install the package and import it once. Importing registers <pdfjs-viewer-element>.

npm install @missing-elements/pdfjs-viewer
import '@missing-elements/pdfjs-viewer'

Or skip the build step and load it from a CDN:

<script type="module" src="https://cdn.jsdelivr.net/npm/@missing-elements/pdfjs-viewer@{{ pdfjsViewer.major }}/dist/pdfjs-viewer-element.js"></script>

Then add the element. It's a block element with no intrinsic height, so give it one.

<pdfjs-viewer-element
  src="https://missing-elements.github.io/pdfjs-viewer/samples/sample-pdf-file.pdf"
  style="height: 100dvh">
</pdfjs-viewer-element>

Attributes

Configure the viewer with attributes. Most of them apply as soon as they change, so you can drive the viewer from your own UI by setting them:

const viewer = document.querySelector('pdfjs-viewer-element')
viewer.setAttribute('page', '2')
viewer.setAttribute('zoom', 'page-fit')

The Updates column in the tables below says when a change takes effect:

  • Live: immediately. Changing src opens the new file without rebuilding the viewer. page, zoom, pagemode, search and phrase are applied together: changing one also reapplies the others that are set, so with page set the viewer goes back to that page. Unset ones leave the reader's page and sidebar alone.
  • Next load: with the next document the viewer opens.
  • Before connect: read once, when the viewer is built. Set it before you add the element to the page.

Playground

{% playground %}

Document and navigation

{% attributeTable "document" %}

<pdfjs-viewer-element
  src="/file.pdf"
  page="2"
  zoom="page-width"
  pagemode="thumbs">
</pdfjs-viewer-element>

Interface

{% attributeTable "interface" %}

Runtime files

You rarely need these. See Hosting.

{% attributeTable "files" %}

PDF.js resources

Data that only some PDFs need. It ships in the package's dist folder, and the defaults load it from next to the viewer files, so set these only to serve it from somewhere else. Bundled apps are the exception: see PDF.js resources.

{% attributeTable "resources" %}

Methods and properties

initPromise

A promise of { viewerApp }, the viewer's [PDFViewerApplication](https://github.com/mozilla/pdf.js/blob/v{{ pdfjsViewer.pdfjsVersion }}/web/app.js). It resolves once the viewer has initialized. Read it after the element is in the document: before that it resolves to {}. A rebuild (for example a locale change that can't be applied in place) replaces it, so read it again each time you need it.

const viewer = document.querySelector('pdfjs-viewer-element')
const { viewerApp } = await viewer.initPromise

viewerApp.eventBus.on('pagechanging', ({ pageNumber }) => {
  console.log(`Page ${pageNumber} of ${viewerApp.pagesCount}`)
})

setViewerOptions(options)

Sets PDF.js [viewer options](https://github.com/mozilla/pdf.js/blob/v{{ pdfjsViewer.pdfjsVersion }}/web/app_options.js), for example to turn on comments and the signature tool:

const viewer = document.querySelector('pdfjs-viewer-element')
await viewer.setViewerOptions({
  enableComment: true,
  enableSignatureEditor: true,
})

The options are applied while the viewer is being built, so call this right after you add the element. A call made after initPromise has resolved takes effect only when the viewer rebuilds. Options from every call are kept and merged, and applied again whenever the viewer rebuilds, for example after a locale change that can't be applied in place, or when the element moves to another place in the page. It resolves to { viewerOptions } once the viewer is ready.

injectViewerStyles(css)

Adds a stylesheet to the viewer's document. The styles survive viewer rebuilds and can't be removed, except by replacing the element. For examples, see Theming.

iframe

The internal <iframe>, available once the element is connected. Use it when you need the viewer's contentWindow or contentDocument directly.

const viewer = document.querySelector('pdfjs-viewer-element')
await viewer.initPromise
const viewerDocument = viewer.iframe.contentDocument

TypeScript

Types ship with the package. The tag isn't added to HTMLElementTagNameMap, so pass the element type to querySelector yourself:

import '@missing-elements/pdfjs-viewer'
import type { PdfjsViewerElement } from '@missing-elements/pdfjs-viewer'

const viewer = document.querySelector<PdfjsViewerElement>('pdfjs-viewer-element')!
const { viewerApp } = await viewer.initPromise

Theming

Light and dark

The default Paper & Ink theme follows the system color scheme. To force one scheme, set viewer-css-theme to LIGHT or DARK. On this page, the theme button at the top sets this attribute on the preview too.

<pdfjs-viewer-element src="/file.pdf" viewer-css-theme="DARK">
</pdfjs-viewer-element>

Your own colors

The viewer is styled with CSS custom properties. To restyle it, override them with injectViewerStyles. Paper & Ink sets its palette on :root, and on :root[data-color-scheme=dark] when dark is forced, so target both selectors. Any variable you don't override keeps the current scheme's value, so pair a custom palette with a fixed viewer-css-theme. Wrap it in a not (forced-colors: active) media query, as Paper & Ink does: in forced colors mode, such as Windows High Contrast, fixed colors hide the toolbar icons.

const viewer = document.querySelector('pdfjs-viewer-element')
viewer.setAttribute('viewer-css-theme', 'LIGHT')
viewer.injectViewerStyles(`
  @media not all and (forced-colors: active) {
    :root, :root[data-color-scheme] {
      --main-color: #3b2bd9;
      --body-bg-color: #eeedfb;
      --toolbar-bg-color: #dcd9f8;
      --toolbar-border-color: #b9b3f0;
      --toolbar-icon-bg-color: #2a1fa3;
      --field-border-color: #8e85e8;
    }
  }
`)

The full list of variables is at the top of the theme file, dist/paper-and-ink.css. To start from the stock PDF.js look instead, point theme-css-src at an empty stylesheet.

Hide toolbar buttons

Any viewer element can be hidden with CSS. For example, this keeps only the left part of the toolbar:

const viewer = document.querySelector('pdfjs-viewer-element')
viewer.injectViewerStyles(`
  #toolbarViewerMiddle, #toolbarViewerRight { display: none; }
`)

Element ids come from the PDF.js viewer markup and can change when PDF.js updates. Check them after upgrading.

Localization

The interface is in English unless you set locale. The locale file is downloaded from jsDelivr when the viewer starts.

<pdfjs-viewer-element src="/file.pdf" locale="uk">
</pdfjs-viewer-element>

Codes with a region, such as pt-BR and zh-CN, work too, and case doesn't matter. Right-to-left locales such as ar and he also switch the layout direction.

To serve locale files yourself, which is useful under a strict CSP or when you can't rely on third parties, copy them from mozilla-l10n/firefox-l10n (<locale>/toolkit/toolkit/pdfviewer/viewer.ftl) and point locale-src-template at them. {locale} is replaced with the locale's folder name in its original case, such as pt-BR, so keep the folder names as they are.

<pdfjs-viewer-element
  src="/file.pdf"
  locale="pt-BR"
  locale-src-template="/locales/{locale}/viewer.ftl">
</pdfjs-viewer-element>

Hosting

The element loads the viewer at runtime from five files next to pdfjs-viewer-element.js in the package's dist folder: viewer.mjs, pdf.mjs, pdf.worker.min.mjs, viewer.css and paper-and-ink.css. All of them must come from the same release as the element. The data some PDFs need sits beside them.

Bundlers and assets-base

Vite and webpack 5 need no configuration for the viewer. The element references its files as new URL('./file', import.meta.url), so these bundlers emit them as assets and rewrite the URLs. They don't copy the PDF.js data folders, though: if your documents need them, serve the package's dist folder and set assets-base, as below.

esbuild, plain Rollup and other bundlers that don't handle that pattern move the element into your bundle without the files, and the viewer fails with 404s. Serve the package's dist folder as static files and point assets-base at it:

<pdfjs-viewer-element src="/file.pdf" assets-base="/static/pdfjs-viewer/">
</pdfjs-viewer-element>

The same attribute can load the files from a CDN:

<pdfjs-viewer-element
  src="/file.pdf"
  assets-base="https://cdn.jsdelivr.net/npm/@missing-elements/pdfjs-viewer@{{ pdfjsViewer.version }}/dist/">
</pdfjs-viewer-element>

assets-base covers the data folders too. It's read when the viewer is built, so set it before the element is connected. A per-file attribute such as worker-src or c-map-url overrides it for that file.

Static pipelines with hashed file names

If your pipeline renames files, for example with content hashes, set each runtime file URL instead of assets-base:

<pdfjs-viewer-element
  src="/file.pdf"
  pdfjs-lib-src="/static/pdf.3f2a1c.mjs"
  viewer-src="/static/viewer.9b8e7d.mjs"
  worker-src="/static/pdf.worker.min.5c4d3e.mjs"
  viewer-css-src="/static/viewer.1a2b3c.css"
  theme-css-src="/static/paper-and-ink.4d5e6f.css">
</pdfjs-viewer-element>

The data folders are looked up next to viewer-src, and the scripting sandbox next to pdfjs-lib-src. Copy cmaps/, standard_fonts/, wasm/, iccs/, images/ and pdf.sandbox.mjs from dist there and keep their names unhashed, because PDF.js loads them by name. Or point the resource attributes somewhere else.

Worker

By default the worker is the bundled pdf.worker.min.mjs, served from your own origin. PDF.js starts it from a blob: URL, so if your page has a Content Security Policy, allow worker-src 'self' blob:. Without that, PDF.js falls back to running on the main thread, which works but is slower.

Set worker-src only to serve the worker from somewhere else, and use the matching PDF.js version ({{ pdfjsViewer.pdfjsVersion }}). A mismatch stops the viewer with the error The API version "…" does not match the Worker version "…".

<pdfjs-viewer-element
  src="/file.pdf"
  worker-src="https://cdn.jsdelivr.net/npm/pdfjs-dist@{{ pdfjsViewer.pdfjsVersion }}/build/pdf.worker.min.mjs">
</pdfjs-viewer-element>

CMaps, fonts and other resources

Most PDFs render with just the viewer files. Some need extra PDF.js data: CMaps for some CJK text, standard fonts for non-embedded base fonts, WASM decoders for JPEG 2000 and JBIG2 images, ICC color profiles, annotation icons, and the scripting sandbox for PDFs with JavaScript. All of it ships in the package and loads only for documents that need it.

Each resource is loaded from next to the viewer files by default, so nothing needs setting when the element comes from a CDN or you serve dist with assets-base. In bundled apps, serve dist and set assets-base, or point the attributes at your own copy. Relative URLs work, and folder URLs get a missing trailing / added:

<pdfjs-viewer-element
  src="/file.pdf"
  c-map-url="/static/pdfjs/cmaps/"
  standard-font-data-url="/static/pdfjs/standard_fonts/"
  wasm-url="/static/pdfjs/wasm/"
  icc-url="/static/pdfjs/iccs/"
  image-resources-path="/static/pdfjs/images/"
  sandbox-bundle-src="/static/pdfjs/pdf.sandbox.mjs">
</pdfjs-viewer-element>

PDF.js runs the JavaScript in PDFs, such as form calculations, in its sandbox. To turn that off, call setViewerOptions({ enableScripting: false }) right after you add the element.

If your page has a Content Security Policy, the WASM decoders and the scripting sandbox need 'wasm-unsafe-eval' in script-src. Without it, PDF JavaScript doesn't run. Resources fetched from another origin need CORS headers, and must be set before the element connects, because the viewer writes its own CSP when it's built.

Cross-origin PDFs

src can point to another origin if that server sends CORS headers (Access-Control-Allow-Origin). To let large files load progressively in ranges, the server also needs to expose Accept-Ranges, Content-Range, Content-Length and Content-Encoding through Access-Control-Expose-Headers. For PDFs behind authentication, fetch the file yourself and open the bytes.

Frameworks

The element works anywhere HTML does. Three things to remember in every framework:

  • Import the package once, on the client. It needs window, so in server-rendered apps (Next.js, Nuxt, SvelteKit, Astro) import it in a client-only hook or component.
  • Give the element a height.
  • Attributes are strings. Call methods (setViewerOptions, injectViewerStyles) through a ref.

React

{% raw %}

import { useEffect } from 'react'

export function PdfViewer({ src }) {
  useEffect(() => {
    import('@missing-elements/pdfjs-viewer')
  }, [])

  return <pdfjs-viewer-element src={src} style={{ height: '100dvh' }} />
}

{% endraw %}

With TypeScript, declare the tag once:

import type { HTMLAttributes, RefAttributes } from 'react'
import type { PdfjsViewerElement } from '@missing-elements/pdfjs-viewer'

type PdfjsViewerAttribute =
  | 'src' | 'page' | 'zoom' | 'pagemode' | 'search' | 'phrase'
  | 'viewer-css-theme' | 'locale' | 'locale-src-template' | 'iframe-title'
  | 'assets-base' | 'worker-src' | 'pdfjs-lib-src' | 'viewer-src' | 'viewer-css-src' | 'theme-css-src'
  | 'c-map-url' | 'standard-font-data-url' | 'wasm-url' | 'icc-url'
  | 'image-resources-path' | 'sandbox-bundle-src'

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'pdfjs-viewer-element': HTMLAttributes<PdfjsViewerElement> &
        RefAttributes<PdfjsViewerElement> &
        Partial<Record<PdfjsViewerAttribute, string>>
    }
  }
}

React example on CodePen

Vue

Tell the Vue compiler the tag is a custom element:

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: { isCustomElement: (tag) => tag === 'pdfjs-viewer-element' },
      },
    }),
  ],
}
<script setup>
import '@missing-elements/pdfjs-viewer'
defineProps({ src: String })
</script>

<template>
  <pdfjs-viewer-element :src="src" style="height: 100dvh" />
</template>

In Nuxt, set the same compiler option in nuxt.config, and import the package from a client-only plugin:

// nuxt.config.js
export default defineNuxtConfig({
  vue: {
    compilerOptions: { isCustomElement: (tag) => tag === 'pdfjs-viewer-element' },
  },
})
// plugins/pdfjs-viewer.client.js
export default defineNuxtPlugin(() => {
  import('@missing-elements/pdfjs-viewer')
})

Vue example on CodePen

Svelte

<script>
  import { onMount } from 'svelte'
  let { src } = $props()
  onMount(() => import('@missing-elements/pdfjs-viewer'))
</script>

<pdfjs-viewer-element {src} style="height: 100dvh"></pdfjs-viewer-element>

Recipes

Open a PDF from bytes

viewerApp.open({ data }) takes a Uint8Array. Use it for files picked with <input type="file">, PDFs generated in the browser, or files you fetch with credentials.

const viewer = document.querySelector('pdfjs-viewer-element')
const { viewerApp } = await viewer.initPromise

const response = await fetch('https://missing-elements.github.io/pdfjs-viewer/samples/sample-pdf-file.pdf')
viewerApp.open({ data: new Uint8Array(await response.arrayBuffer()) })

The src attribute doesn't change when you open bytes.

Several viewers on one page

Each element is an independent viewer with its own iframe, settings and locale. Give each one a height and a distinct iframe-title.

<pdfjs-viewer-element src="/en.pdf" iframe-title="English contract" style="height: 50dvh">
</pdfjs-viewer-element>
<pdfjs-viewer-element src="/de.pdf" iframe-title="German contract" locale="de" style="height: 50dvh">
</pdfjs-viewer-element>

Accessibility

Name every viewer with iframe-title. Screen readers announce it as the name of the embedded viewer, and something like "Annual report 2026" is far more useful than the default "PDF viewer window".

<pdfjs-viewer-element
  src="/reports/annual-report.pdf"
  iframe-title="Annual report 2026"
  style="height: 100dvh">
</pdfjs-viewer-element>
  • Label the viewer in the surrounding page, for example with a heading.
  • Set locale to match your app's language.
  • If you inject CSS, keep focus indicators visible and contrast sufficient.
  • In forced colors mode, such as Windows High Contrast, Paper & Ink steps aside and PDF.js's own high-contrast styles apply. Keep your own colors out of that mode too, as in Your own colors.
  • The viewer can't fix an inaccessible PDF. Use tagged documents with a sensible reading order and text alternatives.

More in the [accessibility guide]({{ pdfjsViewer.repo }}/blob/master/ACCESSIBILITY.md).

Migrating from pdfjs-viewer-element

The package used to be published as pdfjs-viewer-element, which is now deprecated. Only the package name changed. The <pdfjs-viewer-element> tag, the file names and the API are the same.

npm uninstall pdfjs-viewer-element
npm install @missing-elements/pdfjs-viewer

Then update your imports and CDN URLs:

import '@missing-elements/pdfjs-viewer' // was: import 'pdfjs-viewer-element'
<!-- was: https://cdn.jsdelivr.net/npm/pdfjs-viewer-element/dist/pdfjs-viewer-element.js -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@missing-elements/pdfjs-viewer@{{ pdfjsViewer.major }}/dist/pdfjs-viewer-element.js"></script>

If you're coming from an older version:

  • From 4.3 or earlier: if you worked around region locales with a lowercase code and lowercase locale folders, rename the folders back to their original case, such as pt-BR, and use any case in locale. PDF JavaScript now runs; see PDF.js resources to turn it off. Absolute resource URLs keep working, and debugger-src is gone (it never had an effect).
  • From 3.x: version 4 moved from PDF.js 5 to PDF.js 6. If you set worker-src, point it at pdfjs-dist@{{ pdfjsViewer.pdfjsVersion }}. From 3.0.x, also replace viewer-extra-styles and viewer-extra-styles-urls with injectViewerStyles().
  • From 2.x: PDF.js is now bundled, so remove viewer-path and your copy of the PDF.js prebuild. Replace initialize() with initPromise, and viewer-extra-styles and viewer-extra-styles-urls with injectViewerStyles(). The nameddest, text-layer, disable-* and verbosity attributes are gone:
    • disableRange, disableStream, disableAutoFetch, disableFontFace, textLayerMode and verbosity can be set as PDF.js options with setViewerOptions().
    • For a named destination, call viewerApp.pdfLinkService.goToDestination(name) after initPromise resolves.
    • disable-worker has no replacement.