Web Component
<free-pdf-viewer> is Free PDF SDK as a standard custom element. It works anywhere HTML does:
plain pages, Angular, Lit, Solid, Svelte, server-rendered templates. Attributes and properties
configure the viewer, DOM events report what happens, and whenReady() hands you the full
JavaScript API.
<free-pdf-viewer
base-url="/free-pdf-sdk/"
src="/files/contract.pdf"
mode="annotate"
features='{"stamps": false}'
style="height: 80vh"
></free-pdf-viewer>Install and register
Install the package and copy the viewer files into a folder your site serves:
npm install free-pdf-sdk
npx free-pdf-sdk copy-viewer public/free-pdf-sdkThe second command copies viewer/, version.json and the licences into
public/free-pdf-sdk. Run it again after every upgrade (a postinstall or prebuild script
works well) so the viewer always matches the wrapper. The folder's URL is your base-url, and it
must be on the same origin as the page.
Register the element once, before it is used:
import { defineFreePdfViewer } from "free-pdf-sdk/element";
defineFreePdfViewer(); // registers <free-pdf-viewer>defineFreePdfViewer() is safe to call more than once. Pass a name to register the element under
another tag, for example defineFreePdfViewer("pdf-viewer").
Without a bundler
The release zip contains the same modules next to the viewer/ folder. Serve the unzipped
folder (for example at /free-pdf-sdk/) and import the element from it:
<script type="module">
import { defineFreePdfViewer } from "/free-pdf-sdk/element.js";
defineFreePdfViewer();
</script>Give it a height
The element is display: block with position: relative and a min-height of 200px; the
viewer fills it. Set a height, or the viewer is only 200px tall:
<style>
free-pdf-viewer { height: calc(100vh - 64px); }
</style>Any CSS that sizes a block element works: height, flex: 1 inside a flex column, a grid area.
Attributes
| Attribute | Value | Becomes |
|---|---|---|
base-url |
URL of the viewer folder, on the page's origin. Required: nothing mounts without it. | baseUrl |
src |
URL of a PDF to open (same-origin, or a CORS-enabled URL). | document: { url } |
mode |
view, review, annotate, fill, sign, organize, edit or full. Default view (read-only). |
mode |
features |
JSON object of feature switches on top of the mode, such as {"stamps": false}. |
features |
ui |
JSON object of interface options, such as {"toolsPanel": false}. |
ui |
disabled-elements |
Element ids separated by commas or spaces, such as download print tool:stamp. |
disabledElements |
theme |
light, dark or system (default). |
theme |
author |
Author name of new comments. | author |
locale |
Interface language, such as en, de or zh-Hans. |
locale |
password |
Password used when src is opened (opens a protected file and lifts owner restrictions). |
password |
loader |
URL of a loading image (SVG, GIF, animated WebP or PNG), or none. Read when the viewer mounts. |
loader |
If features or ui is not valid JSON, the element dispatches an fps-error event with code
BAD_REQUEST and ignores the attribute.
Changing an attribute later calls the matching API method on the running viewer (mode calls
setMode, features calls setFeatures, src opens the new document, and so on). Changes made
in the same task are batched. Changing base-url destroys the viewer and mounts a new one.
const el = document.querySelector("free-pdf-viewer");
el.setAttribute("mode", "review");
el.setAttribute("disabled-elements", "download, print");Two details follow from how changes map to calls:
featuresreplaces the overrides: removing the attribute callssetFeatures({}), which goes back to the mode's own features.uioptions you leave out keep their current value.passwordis only read when a document is opened. Changing it alone does nothing; it is used the next timesrcchanges.
Properties
The element has properties for the options you are most likely to set from code:
| Property | Type | Notes |
|---|---|---|
baseUrl |
string |
Same as base-url. |
mode |
Mode |
Same as mode. |
features |
FeatureFlags |
An object; no JSON needed. |
ui |
UiOptions |
An object; no JSON needed. |
theme |
Theme |
Same as theme. |
options |
Partial<ViewerOptions> |
Sets several options at once (see below). |
viewer |
FreePdfViewer | null |
Read-only. The viewer's API once it is ready, else null. |
const el = document.querySelector("free-pdf-viewer");
el.features = { stamps: false, measurement: false };
el.ui = { quickToolbar: false, panels: { bookmarks: false } };A value set through a property wins over the attribute of the same name, and stays in force:
once you set el.mode, later changes to the mode attribute are ignored.
The options setter
options takes any of the FreePdf.init options except listeners: baseUrl, document,
mode, features, ui, theme, author, locale, password, disabledElements,
loader and timeoutMs. What you pass is merged into what was set before, and it wins over attributes.
Use it for everything that has no property of its own, most usefully a document given as bytes:
const input = document.querySelector("input[type=file]");
input.addEventListener("change", () => {
const file = input.files[0];
if (!file) return;
el.options = {
document: { data: file, name: file.name },
author: "Priya Raman",
disabledElements: ["download", "print"],
};
});Reading el.options returns a copy of the options set through properties (not the attributes).
Events
The element dispatches a DOM CustomEvent for every viewer event. The event's data is in
detail. The events bubble and cross shadow roots (bubbles and composed).
| DOM event | detail |
When |
|---|---|---|
fps-ready |
{ sdk, pdfcraftRev } |
The viewer is configured and ready for requests. |
fps-document-loaded |
{ pageCount, permissions } |
A document opened. |
fps-document-error |
{ code, message } |
A document could not be opened. |
fps-annotations-changed |
{ count } |
Comments were added, changed or removed, by the user or the API. |
fps-dirty-changed |
{ dirty } |
The document gained or lost unsaved changes. |
fps-error |
{ code, message } |
Something failed outside a request: the viewer could not start, an option change was refused, an attribute held invalid JSON. |
fps-save-requested |
{ name, saveAs, data } |
The user pressed Save in the viewer. data is the saved PDF as a Uint8Array; the viewer now treats it as saved. |
permissions is { canAnnotate, canPrint, canCopy, isEncrypted, canUnlock }.
el.addEventListener("fps-document-loaded", (e) => {
console.log(`${e.detail.pageCount} pages`, e.detail.permissions);
});
el.addEventListener("fps-annotations-changed", (e) => {
document.querySelector("#count").textContent = String(e.detail.count);
});
el.addEventListener("fps-save-requested", async (e) => {
const { name, data } = e.detail;
await fetch(`/api/files/${encodeURIComponent(name)}`, { method: "PUT", body: data });
});
el.addEventListener("fps-error", (e) => {
console.error(e.detail.code, e.detail.message);
});Add listeners before the element is connected (or in the same task) to hear fps-ready and the
first fps-document-loaded.
Call the API
For everything else (export comments, save, navigate) use the viewer's API. whenReady()
resolves with it once the viewer is ready and the document from src is open; viewer returns
it synchronously, or null before then.
const el = document.querySelector("free-pdf-viewer");
document.querySelector("#export").addEventListener("click", async () => {
const viewer = await el.whenReady();
const xfdf = await viewer.exportXfdf();
await fetch("/api/annotations/contract-42", {
method: "PUT",
headers: { "Content-Type": "application/vnd.adobe.xfdf" },
body: xfdf,
});
});whenReady() can be called at any time, even before the element is connected: it resolves
once the viewer is ready and its first document has loaded (or failed to), or rejects if it can't start.
const el = document.createElement("free-pdf-viewer");
el.setAttribute("base-url", "/free-pdf-sdk/");
el.setAttribute("src", "/files/contract.pdf");
el.style.height = "80vh";
document.querySelector("main").append(el);
const viewer = await el.whenReady();
console.log(await viewer.getPageCount());Calls made through the API don't update the element's attributes or properties. If you call
viewer.setMode("review") and later change the mode attribute, the attribute's value is
applied then.
A complete page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Contract review</title>
<style>
body { margin: 0; display: flex; flex-direction: column; height: 100vh; font-family: system-ui, sans-serif; }
header { display: flex; gap: 12px; align-items: center; padding: 8px 16px; }
free-pdf-viewer { flex: 1; }
</style>
</head>
<body>
<header>
<select id="mode">
<option value="view">View</option>
<option value="review">Review</option>
<option value="annotate" selected>Annotate</option>
</select>
<span><span id="count">0</span> comments</span>
<button id="export">Save comments</button>
</header>
<free-pdf-viewer
base-url="/free-pdf-sdk/"
src="/files/contract.pdf"
mode="annotate"
author="Priya Raman"
disabled-elements="download"
></free-pdf-viewer>
<script type="module">
import { defineFreePdfViewer } from "/free-pdf-sdk/element.js";
defineFreePdfViewer();
const el = document.querySelector("free-pdf-viewer");
el.addEventListener("fps-annotations-changed", (e) => {
document.querySelector("#count").textContent = String(e.detail.count);
});
el.addEventListener("fps-error", (e) => console.error(e.detail));
document.querySelector("#mode").addEventListener("change", (e) => {
el.setAttribute("mode", e.target.value);
});
document.querySelector("#export").addEventListener("click", async () => {
const viewer = await el.whenReady();
const xfdf = await viewer.exportXfdf();
await fetch("/api/annotations/contract", { method: "PUT", body: xfdf });
});
</script>
</body>
</html>Remove it
Removing the element from the DOM destroys its viewer: the iframe goes away and pending calls
reject with DESTROYED. Inserting the element again mounts a new viewer.
Things to know
- The viewer runs in an iframe served from
base-url, on your page's origin, and talks to the page overpostMessage. Abase-urlon another origin is refused withBAD_REQUEST. - PDFs are processed in the browser with WebAssembly; the SDK does not upload them anywhere.
- The viewer needs WebGL. If it can't start,
fps-errorreportsTIMEOUT(usually a wrongbase-url) orINTERNAL(the viewer loaded but could not start, for example without WebGL). - If a document can't be opened,
fps-document-errorreports why and the viewer keeps running: set anothersrc(orpassword) to try again. - TypeScript: the element class is exported as
FreePdfViewerElementfromfree-pdf-sdk/element, and the DOM event names asELEMENT_EVENTS.
Configuration lists every mode, feature, ui option and
disabled-elements id; the API reference lists every method of the viewer.
Using a framework? See Angular and Svelte, or the dedicated React and Vue components.