EmbedPDF

Opening documents

Give the viewer the documents to open when it starts, and <DocumentGate> shows your content once the document is ready:

initial-documents.tsx
import { DocumentGate, Viewer } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { engine, plugins } from './pdf';
import { Spinner } from './Spinner';

export function ContractViewer() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: { kind: 'url', url: '/files/contract.pdf' } }]}
    >
      <DocumentGate fallback={<Spinner />}>
        <Stage>{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

The viewer is on screen at once; the document opens in the background. Components outside <DocumentGate>, such as your toolbar’s frame, render right away.

Open a document later#

useDocuments() opens documents while the app runs, for example one the user picked:

open-button.tsx
import { useDocuments } from '@embedpdf/react/runtime';

export function OpenButton() {
  const documents = useDocuments();

  const onPick = async (file: File) => {
    await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
  };

  return <input type="file" accept="application/pdf" onChange={(e) => onPick(e.target.files![0])} />;
}

open() resolves with { document } once it’s open, and the new document becomes the active one. Pass { activate: false } to open it in the background. Pick a PDF of your own here:

Loading live preview…

import { Viewer, DocumentGate, useDocument, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './open.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

function Toolbar() {
  const documents = useDocuments();
  const { id, name, status, pageCount } = useDocument();

  // A file the user picks opens next to the welcome, and becomes the active document.
  const openFile = async (file: File) => {
    await documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
  };

  return (
    <div className="toolbar">
      <span className="document">
        {id ? (
          <>
            <strong>{name ?? 'Untitled'}</strong>
            {status === 'ready' ? ` · ${pageCount} pages` : ` · ${status}`}
          </>
        ) : (
          'No document open'
        )}
      </span>
      <label className="button picker">
        Open a PDF…
        <input
          type="file"
          accept="application/pdf"
          onChange={(event) => {
            const file = event.target.files?.[0];
            if (file) void openFile(file);
            event.target.value = '';
          }}
        />
      </label>
      {id ? (
        <button type="button" className="button" onClick={() => documents.close(id)}>
          Close
        </button>
      ) : (
        <button
          type="button"
          className="button"
          onClick={() => documents.open(welcome, { name: 'welcome.pdf' })}
        >
          Open the welcome again
        </button>
      )}
    </div>
  );
}

// The gate's fallback: a document on its way, or none at all.
function Empty() {
  const { id } = useDocument();
  return (
    <p className="loading">{id ? 'Opening…' : 'No document is open. Open a PDF to see it here.'}</p>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: welcome, name: 'welcome.pdf' }]}
    >
      <Toolbar />
      <DocumentGate fallback={<Empty />}>
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

Where a document comes from#

SourceWhat it is
{ kind: 'url', url }A file the viewer fetches. The URL can be relative to your site.
{ kind: 'bytes', bytes }A file you already have: an upload, a file picker, a download.
async (signal) => sourceAnything else: a function that returns one of the above.

The viewer calls a function under the document’s loading state, and signal fires if the user closes the document first, so pass it on to what the function waits for.

Files that need a sign-in#

When your server wants a header or a cookie for a file, give the viewer your own fetch. Every { kind: 'url' } document downloads through it: it gets the URL, and an init with the document’s cancel signal to pass on.

fetch-headers.tsx
import { DocumentGate, Viewer } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { getToken } from './auth';
import { engine, plugins } from './pdf';

export function ContractViewer() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: { kind: 'url', url: '/api/files/contract.pdf' } }]}
      fetch={(url, init) =>
        fetch(url, { ...init, headers: { Authorization: `Bearer ${getToken()}` } })
      }
    >
      <DocumentGate>
        <Stage>{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

For your cookies on another site, add credentials: 'include' instead; that site’s CORS headers have to allow it. A server that answers 401 or 403 makes the document’s error permission-denied, and documents.retry(id) calls your fetch again, so a token it reads when it’s called is a fresh one. The engine’s own files never go through it.

Loading, errors and passwords#

<DocumentGate> has something to show for each state a document can be in:

gate-states.tsx
import { DocumentGate } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { PasswordForm } from './PasswordForm';
import { Spinner } from './Spinner';

export function DocumentView() {
  return (
    <DocumentGate
      fallback={<Spinner />}
      locked={(document) => <PasswordForm document={document} />}
      error={(document) => <p>Couldn’t open {document.name}: {document.error.message}</p>}
    >
      <Stage>{() => <RenderLayer />}</Stage>
    </DocumentGate>
  );
}

A password-protected PDF opens locked. Ask for the password and unlock it:

password-form.tsx
import { useState } from 'react';
import { useDocuments, type DocumentInfo } from '@embedpdf/react/runtime';
import { PasswordPrompt } from './PasswordPrompt';

export function PasswordForm({ document }: { document: DocumentInfo }) {
  const documents = useDocuments();
  const [wrong, setWrong] = useState(false);

  const submit = async (password: string) => {
    try {
      await documents.unlock(document.id, { password });
    } catch {
      setWrong(true); // it stays locked; ask again
    }
  };

  return <PasswordPrompt onSubmit={submit} wrong={wrong} />;
}

After an error, such as a network failure, documents.retry(id) tries again. Here the second document isn’t a PDF, so it shows its error:

Loading live preview…

import { useState } from 'react';
import {
  Viewer,
  DocumentGate,
  useDocuments,
  useDocumentsState,
  type DocumentInfo,
  type FailedDocumentInfo,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './states.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// A file that isn't a PDF, so its document can't be opened.
const broken: OpenInput = { kind: 'bytes', bytes: new TextEncoder().encode('Not a PDF') };

function Tabs() {
  const documents = useDocuments();
  const { documents: open, activeId } = useDocumentsState();

  return (
    <div className="tabs" role="tablist">
      {open.map((document) => (
        <button
          key={document.id}
          type="button"
          role="tab"
          className="tab"
          aria-selected={document.id === activeId}
          onClick={() => documents.setActive(document.id)}
        >
          <span className="status" data-status={document.status} />
          {document.name}
        </button>
      ))}
    </div>
  );
}

function PasswordForm({ document }: { document: DocumentInfo }) {
  const documents = useDocuments();
  const [password, setPassword] = useState('');
  const [wrong, setWrong] = useState(document.passwordProvided ?? false);

  const unlock = async () => {
    try {
      await documents.unlock(document.id, { password });
    } catch {
      setWrong(true); // it stays locked; ask again
    }
  };

  return (
    <div className="panel">
      <h3>{document.name} needs a password</h3>
      {wrong && <p>That password isn’t right. Try again.</p>}
      <div className="actions">
        <input
          className="field"
          type="password"
          aria-label="Password"
          value={password}
          onChange={(event) => setPassword(event.target.value)}
        />
        <button type="button" className="button" onClick={unlock}>
          Unlock
        </button>
      </div>
    </div>
  );
}

function OpenError({ document }: { document: FailedDocumentInfo }) {
  const documents = useDocuments();
  return (
    <div className="panel" data-tone="error">
      <h3>Couldn’t open {document.name}</h3>
      <p>{document.error.message}</p>
      <div className="actions">
        <button type="button" className="button" onClick={() => documents.retry(document.id)}>
          Try again
        </button>
        <button type="button" className="button" onClick={() => documents.close(document.id)}>
          Close
        </button>
      </div>
    </div>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[
        { source: welcome, name: 'welcome.pdf' },
        { source: broken, name: 'broken.pdf', active: true },
      ]}
    >
      <Tabs />
      <DocumentGate
        fallback={<div className="panel">Opening…</div>}
        locked={(document) => <PasswordForm document={document} />}
        error={(document) => <OpenError document={document} />}
      >
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}

To show the state yourself, for example on a tab, useDocument() gives you the document’s status: 'loading', 'locked', 'ready' or 'error'. Every field is listed under State.

Once a document is 'ready', each page’s picture can still take a moment to arrive. To show that too, see Show that a page is loading.

Who the user is and what they may do#

Tell the viewer who the user is and what they may do, once. Every document it opens uses them:

identity-scope.tsx
import { Viewer } from '@embedpdf/react/runtime';
import { engine, plugins } from './pdf';

export function ReviewViewer() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      identity={{ userId: 'u_381', displayName: 'Dana Smith' }}
      scope={['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read', 'annotations:create:self']}
    />
  );
}
  • identity is who the user is. Their displayName becomes the author of the annotations they make.
  • scope is what they may do, as a list of permissions. Leave it out and they may do what the file allows: everything, unless the PDF restricts itself, as in Acrobat.

Loading live preview…

import { useState } from 'react';
import { Viewer, DocumentGate, useDocument, useDocuments } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './identity.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

const welcome = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/welcome.pdf');
  return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const dana = { userId: 'u_381', displayName: 'Dana Smith' };

// What each role may do, as permissions.
const roles = {
  reader: ['doc.open', 'doc.render', 'doc.text.select'],
  editor: [
    'doc.open',
    'doc.render',
    'doc.text.select',
    'doc.text.copy',
    'doc.download',
    'doc.print',
  ],
};
type Role = keyof typeof roles;

function Toolbar({ role, onRoleChange }: { role: Role; onRoleChange: (role: Role) => void }) {
  const documents = useDocuments();
  // Read the document too, so the checks are read again when it opens or closes.
  const { id } = useDocument();
  // The role the open document was opened with: it keeps that one.
  const [openedAs, setOpenedAs] = useState(role);

  const openAgain = async () => {
    await documents.close(id);
    await documents.open(welcome, { name: 'welcome.pdf' });
    setOpenedAs(role);
  };

  return (
    <>
      <div className="toolbar">
        <label className="label">
          Dana Smith, as
          <select
            className="select"
            value={role}
            onChange={(event) => onRoleChange(event.target.value as Role)}
          >
            <option value="reader">reader</option>
            <option value="editor">editor</option>
          </select>
        </label>
        <span className="check" data-allowed={documents.canDownload()}>
          Download
        </span>
        <span className="check" data-allowed={documents.canPrint()}>
          Print
        </span>
      </div>
      <p className="note">
        This document opened for a {openedAs}.
        {openedAs !== role && (
          <button type="button" className="button" onClick={openAgain}>
            Open it again as {role}
          </button>
        )}
      </p>
    </>
  );
}

export default function App() {
  const [role, setRole] = useState<Role>('reader');
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      identity={dana}
      scope={roles[role]}
      initialDocuments={[{ source: welcome, name: 'welcome.pdf' }]}
    >
      <Toolbar role={role} onRoleChange={setRole} />
      <DocumentGate fallback={<p className="loading">Opening…</p>}>
        <Stage className="stage">{() => <RenderLayer />}</Stage>
      </DocumentGate>
    </Viewer>
  );
}
Change them on the viewer, and the documents you open after that use the new values; documents already open keep theirs.

A document can have its own, which replace the viewer’s for that document:

const initialDocuments = [{ source, scope: ['doc.open', 'doc.render'] }]; // this one is read-only
await documents.open(source, { scope: ['doc.open', 'doc.render'] }); // and so is this one

Permissions lists every permission.

Close a document#

await documents.close(document.id);

Closing frees the document’s memory in the engine. The next document becomes the active one.

Methods#

What useDocuments() gives you for opening and closing documents and reading their pages. Tabs and the active document are on Several documents, downloading and printing on Saving & downloading.

MethodWhat it does
open(source, options?)Opens a document and makes it the active one. Resolves { document } once it’s ready, or locked with a password; rejects when it can’t be opened. Open a document later
canOpenFiles()Whether open() takes a file’s bytes, such as a file the user picked, or a URL it downloads: true on the local engine, false on CloudPDF, where every document comes from your server
openAll(documents)Opens several documents, as initialDocuments does: every tab appears at once, in order, and the one marked active (or the first) becomes the active one. A failure shows in that document’s status instead of throwing.
unlock(id, { password })Unlocks a locked document. Rejects on a wrong password, and the document stays locked so you can ask again. Loading, errors and passwords
retry(id, options?)Tries a failed document again, with the same source and options. Resolves like open(). A document from a grant takes what it waited for: { passphrase } or { identity }. Loading, errors and passwords
identify(id, answers)Answers what the document’s grant asks before the first change (its identityRequest): the change waiting goes out. Rejects while a required answer is missing or one can’t be taken, and the change keeps waiting.
declineIdentity(id)Takes back the change waiting for the visitor’s answers; the next change asks again.
close(id), closeAll()Closes one document, or every document, and frees its memory. Close a document
get(id?)A document’s fields, the ones useDocument() gives you, or null when it isn’t open. Without an id, the document in scope
listPages(id?)The document’s pages, in order. In a component, use usePageList().
getPage(page, id?)One page, by its ref or its index, or null when the document doesn’t have it
getPageIndex(ref, id?)A page’s position, from 0, or -1 when the document doesn’t have it
getRevision(id?)A number that goes up each time pages are added, removed, moved or rotated; -1 with no document

options for open() are name (what its tab shows), activate: false (open it in the background), password, identity (who your page knows the user is), and, on the local engine, scope. Without an id, the page methods use the document in scope: the one a <DocumentScope> around your component names, or else the active one. To stop an open that’s still loading, close the document: that cancels its download too.

State#

useDocument() gives you the document a component talks to: the active one, or the one its <DocumentScope> names. Your component re-renders when a value changes. Outside a component, get(id) gives the same fields. With no document open yet, it reads as one still on its way: an empty id and status: 'loading', so a component that shows it never has to check for null.

FieldWhat it is
idThe document’s id, for close(id), unlock(id, …) and <DocumentScope>
nameThe name its tab shows, from open() or rename()
status'loading', 'locked', 'ready' or 'error'
pageCountHow many pages it has; 0 until it’s ready
hasUnsavedChangesWhether it has changes that weren’t downloaded yet. Warn before changes are lost
passwordProvidedWhen it’s locked: whether a password was tried and was wrong
errorWhy it couldn’t be opened, when status is 'error': { code, message }
grantRequestWhen it’s from a grant: the passphrase or the questions it waits for, to open or to go on
identityRequestWhen it’s ready and a change waits for the visitor’s answers to the grant’s questions

Every open document, with the same fields, comes from useDocumentsState().

Events#

open-failed.tsx
import { useState } from 'react';
import { useDocumentsEvent } from '@embedpdf/react/runtime';

export function OpenError() {
  const [message, setMessage] = useState('');

  useDocumentsEvent(
    (documents) => documents.onOpenFailed,
    ({ error }) => setMessage(`Couldn't open the file: ${error.message}`),
  );

  return <p role="alert">{message}</p>;
}
EventFires whenCarries
onOpenedA document is open and readydocumentId, info
onOpenFailedA document couldn’t be openeddocumentId, error
onLockedA document needs a passworddocumentId, passwordProvided
onClosedA document was closeddocumentId
onPagesChangedPages were added, removed, moved or rotateddocumentId, pages, revision

Switching the active document is on Several documents.

Good to know#

Opening large files#

The local engine needs the whole file before it can show the first page, so a large PDF on a slow connection takes as long as its download.

initialDocuments is read once#

Like engine and plugins, the viewer reads initialDocuments when it mounts. To open another document later, use documents.open().

Several documents#

A viewer can hold several documents at once, like tabs. See Several documents.

Was this page helpful?

Your feedback goes directly to the documentation team.