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:
- Live: immediately. Changing
srcopens the new file without rebuilding the viewer.page,zoom,pagemode,searchandphraseare applied together. Changing any of them also goes to the page inpage(page 1 when it's unset) and resets the sidebar topagemode, closing a sidebar the reader opened. - 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
Document and navigation
| Attribute | Description | Default | Updates |
|---|---|---|---|
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 |
search |
Text to search for and highlight. | 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
| Attribute | Description | Default | Updates |
|---|---|---|---|
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.
| Attribute | Description | Default | Updates |
|---|---|---|---|
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.
| Attribute | Description | Default | Updates |
|---|---|---|---|
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-ESandsv-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 aspt-br, with a self-hostedlocale-src-templatethat 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:
- 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
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>>
}
}
}
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')
})
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
localeto match your app's language. - If you inject CSS, keep focus indicators visible and contrast sufficient.
- The viewer can't fix an inaccessible PDF. Use tagged documents with a sensible reading order and text alternatives.
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:
- From 3.x: version 4 moved from PDF.js 5 to PDF.js 6. If you set
worker-src, point it atpdfjs-dist@6.4.299. From 3.0.x, also replaceviewer-extra-stylesandviewer-extra-styles-urlswithinjectViewerStyles(). - From 2.x: PDF.js is now bundled, so remove
viewer-pathand your copy of the PDF.js prebuild. Replaceinitialize()withinitPromise, andviewer-extra-stylesandviewer-extra-styles-urlswithinjectViewerStyles(). Thenameddest,text-layer,disable-*andverbosityattributes are gone:disableRange,disableStream,disableAutoFetch,disableFontFace,textLayerModeandverbositycan be set as PDF.js options withsetViewerOptions().- For a named destination, call
viewerApp.pdfLinkService.goToDestination(name)afterinitPromiseresolves. disable-workerhas no replacement.