Missing Elements

pdfjs-viewer

A self-hostable PDF viewer web component, based on the PDF.js default viewer.

On this page

The full PDF.js viewer as one HTML element. It runs in an isolated iframe and has no runtime dependencies. PDF.js and its worker 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@4/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:

Playground

Each change applies to the live preview straight away.

Document and navigation

Document and navigation attributes
AttributeDescriptionDefaultUpdates
src PDF file URL, relative to the page or absolute. Cross-origin URLs need CORS headers on the server. An empty value keeps the current document. None Live
page Page number to show. None Live
zoom auto, page-fit, page-width, page-actual, or a percentage such as 200. None Live
pagemode Sidebar: none, thumbs, bookmarks, attachments or layers. none Live
phrase true matches search as a whole phrase instead of separate words. None Live
<pdfjs-viewer-element
  src="/file.pdf"
  page="2"
  zoom="page-width"
  pagemode="thumbs">
</pdfjs-viewer-element>

Interface

Interface attributes
AttributeDescriptionDefaultUpdates
viewer-css-theme AUTOMATIC follows the system color scheme, LIGHT and DARK force one. AUTOMATIC Live
locale Interface language, such as de, uk or ja. Codes outside the PDF.js locale list are ignored. None Live
locale-src-template Absolute URL of the locale file, with a {locale} placeholder. Use it to self-host locales. Read when the viewer is built and when locale changes. https://cdn.jsdelivr.net/gh/mozilla-l10n/firefox-l10n@main/{locale}/toolkit/toolkit/pdfviewer/viewer.ftl Before connect
iframe-title Accessible name of the internal iframe. Describe the document. PDF viewer window Live

Runtime files

You rarely need these. See Hosting.

Runtime file attributes
AttributeDescriptionDefaultUpdates
assets-base Folder URL holding the five runtime files. Only needed when your bundler doesn't emit them, or to load them from a CDN. See Bundlers. Folder of pdfjs-viewer-element.js Before connect
worker-src PDF.js worker module URL. Must match the bundled PDF.js version. Bundled pdf.worker.min.mjs Next load
pdfjs-lib-src PDF.js library module URL. Bundled pdf.mjs Before connect
viewer-src PDF.js viewer module URL. Bundled viewer.mjs Before connect
viewer-css-src PDF.js viewer stylesheet URL. Bundled viewer.css Before connect
theme-css-src Paper & Ink theme stylesheet URL. Bundled paper-and-ink.css Before connect

PDF.js resources

Data that only some PDFs need. The package doesn't include it, and the defaults are relative paths that don't work inside the viewer. See PDF.js resources before you set these.

PDF.js resource attributes
AttributeDescriptionDefaultUpdates
c-map-url Absolute URL of the CMaps folder, ending in /. Needed for some CJK text. ../web/cmaps/ Next load
standard-font-data-url Absolute URL of the standard fonts folder, ending in /. For PDFs that use non-embedded base fonts. ../web/standard_fonts/ Next load
wasm-url Absolute URL of the WASM image decoders folder (JPEG 2000, JBIG2), ending in /. ../web/wasm/ Next load
icc-url Absolute URL of the ICC color profiles folder, ending in /. ../web/iccs/ Next load
image-resources-path Folder of annotation icons (annotation-*.svg), shipped in the package as dist/images/. The default is relative to your page, so set this or assets-base. ./images/, or assets-base + images/ Before connect
sandbox-bundle-src PDF.js scripting sandbox URL, for PDFs with JavaScript (forms). ../build/pdf.sandbox.mjs Next load
debugger-src PDF.js debugger script URL. It has no effect: the debugger isn't shipped in the package, and the element offers no way to turn it on. ./debugger.mjs No effect

Methods and properties

initPromise

A promise of { viewerApp }, the viewer's PDFViewerApplication. 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, 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 has no effect until the viewer rebuilds. Each call replaces the options of the previous one, and the options are cleared once applied. A rebuild (a locale change that can't be applied in place, or moving the element to another place in the page) drops them, so call it again after one. 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:

const viewer = document.querySelector('pdfjs-viewer-element')
viewer.setAttribute('viewer-css-theme', 'LIGHT')
viewer.injectViewerStyles(`
  :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>

Right-to-left locales such as ar and he also switch the layout direction.

Known issue in v4.3.0: codes are checked against a lowercase list, so codes with a region, such as pt-BR, zh-CN, es-ES and sv-SE, are ignored and the viewer stays in English. These languages have no language-only code to fall back on. As a workaround, use the lowercase code, such as pt-br, with a self-hosted locale-src-template that uses the same lowercase folder name.

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. The template must be an absolute URL: a relative one breaks all of the viewer's interface text.

<pdfjs-viewer-element
  src="/file.pdf"
  locale="de"
  locale-src-template="https://example.com/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.

Bundlers and assets-base

Vite and webpack 5 need no configuration. The element references its files as new URL('./file', import.meta.url), so these bundlers emit them as assets and rewrite the URLs.

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@4.3.0/dist/">
</pdfjs-viewer-element>

assets-base is read when the viewer is built, so set it before the element is connected. A per-file attribute such as worker-src overrides it for that file.

Annotation icons need this whichever bundler you use. PDF.js loads them by name at runtime (annotation-<name>.svg), so no bundler emits them, and the default path is relative to your page. Serve the package's dist/images folder and point image-resources-path at it, or set assets-base, which covers the icons too.

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"
  image-resources-path="/static/pdfjs-images/">
</pdfjs-viewer-element>

Keep the dist/images folder unhashed: annotation icons are loaded by name.

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 (6.4.299). 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@6.4.299/build/pdf.worker.min.mjs">
</pdfjs-viewer-element>

CMaps, fonts and other resources

Most PDFs render with just the package. 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, and the scripting sandbox for PDFs with JavaScript. The package doesn't include these, and the defaults are relative paths that don't work inside the viewer.

If your documents need them, copy the folders from pdfjs-dist@6.4.299 into your static files and point the attributes at them. Use absolute URLs that end in /. PDF.js fetches these folders from its worker, which runs from a blob: URL where relative paths don't resolve, and it rejects a folder URL without the trailing slash.

<pdfjs-viewer-element
  src="/file.pdf"
  c-map-url="https://example.com/pdfjs/cmaps/"
  standard-font-data-url="https://example.com/pdfjs/standard_fonts/"
  wasm-url="https://example.com/pdfjs/wasm/"
  icc-url="https://example.com/pdfjs/iccs/"
  sandbox-bundle-src="https://example.com/pdfjs/build/pdf.sandbox.min.mjs">
</pdfjs-viewer-element>

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:

React

import { useEffect } from 'react'

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

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

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' | 'debugger-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>

More in the accessibility guide.

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@4/dist/pdfjs-viewer-element.js"></script>

If you're coming from an older major version: