EmbedPDF

Single pages & thumbnails

<PageView> shows one page on its own, without scrolling or zoom: a preview in a card, a page picker in a dialog, the page a comment belongs to. Give it a page and a width:

page-preview.tsx
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer } from '@embedpdf/react/render';
import type { PageRef } from '@embedpdf/react/runtime';

export function PagePreview({ page }: { page: PageRef }) {
  return (
    <PageView page={page} width={240}>
      <RenderLayer />
    </PageView>
  );
}

Loading live preview…

import { useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

const engine = localEngine();
// No Stage: a page on its own needs only the render plugin.
const plugins = [renderPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// A card that shows one page, like a preview next to a search result or a comment.
function PageCard() {
  const pages = usePageList();
  const [index, setIndex] = useState(0);
  const page = pages[index];

  return (
    <figure className="card">
      <PageView page={index} width={220} className="page">
        <RenderLayer />
      </PageView>
      <figcaption className="caption">
        <strong className="title">Page {page?.label ?? index + 1}</strong>
        {page ? (
          <span className="detail">
            {Math.round(page.size.width)} × {Math.round(page.size.height)} points
          </span>
        ) : null}
        <span className="pager">
          <button
            type="button"
            className="button"
            disabled={index === 0}
            onClick={() => setIndex(index - 1)}
          >
            ‹ Previous
          </button>
          <button
            type="button"
            className="button"
            disabled={index >= pages.length - 1}
            onClick={() => setIndex(index + 1)}
          >
            Next ›
          </button>
        </span>
      </figcaption>
    </figure>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <PageCard />
      </DocumentGate>
    </Viewer>
  );
}

Every layer works in <PageView> as it does on the Stage: selection, annotations, search highlights. So does a menu anchored to the page. The page keeps its rotation and redraws when it changes.

Pick the page#

page takes a page’s ref or its index, so 0 is the first page:

first-page.tsx
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer } from '@embedpdf/react/render';

export const FirstPage = () => (
  <PageView page={0} width={320}>
    <RenderLayer />
  </PageView>
);

usePageList() gives you every page of the document, in order, to loop over:

page-picker.tsx
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer } from '@embedpdf/react/render';
import { usePageList, type PageRef } from '@embedpdf/react/runtime';

export function PagePicker({ onPick }: { onPick: (page: PageRef) => void }) {
  const pages = usePageList();
  return pages.map((page) => (
    <button key={page.index} onClick={() => onPick(page.ref)}>
      <PageView page={page.ref} width={120}>
        <RenderLayer />
      </PageView>
      {page.label ?? page.index + 1}
    </button>
  ));
}

Loading live preview…

import { useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './picker.css';

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

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

function PagePicker() {
  const pages = usePageList();
  const [picked, setPicked] = useState<PageRef | null>(null);
  // A ref follows its page when pages move, so it's what you keep.
  const shown = picked ?? pages[0]?.ref ?? null;

  return (
    <div className="picker">
      <div className="choices" role="listbox" aria-label="Pages">
        {pages.map((page) => (
          <button
            key={page.ref.objectNumber}
            type="button"
            role="option"
            className="choice"
            aria-selected={page.ref.objectNumber === shown?.objectNumber}
            onClick={() => setPicked(page.ref)}
          >
            <PageView page={page.ref} width={84}>
              <RenderLayer />
            </PageView>
            <span className="number">{page.label ?? page.index + 1}</span>
          </button>
        ))}
      </div>
      <div className="shown">
        {shown ? (
          <PageView page={shown} width={300}>
            <RenderLayer />
          </PageView>
        ) : null}
      </div>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <PagePicker />
      </DocumentGate>
    </Viewer>
  );
}

A page’s label is the page number the PDF defines, such as 'iv' or 'A-2', when it has one.

A thumbnail strip#

For a strip next to the main view, with many pages that scroll, use a second Stage rather than many <PageView>s: it mounts only the thumbnails you can see, and it can follow the page the reader is on.

thumbnail-strip-token.ts
import { createCapabilityToken } from '@embedpdf/react/runtime';
import { stagePlugin, type StageCapability } from '@embedpdf/react/stage';

export const ThumbsToken = createCapabilityToken<StageCapability>('stage-thumbs');

export const plugins = [
  stagePlugin(),
  stagePlugin({
    id: 'stage-thumbs',
    token: ThumbsToken,
    interaction: false, // a drag doesn't select text or draw
    zoomGestures: false, // a pinch doesn't resize the thumbnails
    zoom: { pageWidth: 120 },
    gap: { px: 12 },
    pageFrame: { bottom: 20 }, // room for the page number
  }),
];
thumbnail-strip.tsx
import { useEffect } from 'react';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage, useStage, useStageState } from '@embedpdf/react/stage';
import { ThumbsToken } from './thumbnail-strip-token';

export function Thumbnails() {
  const main = useStage();
  const thumbs = useStage(ThumbsToken);
  const { currentPageIndex } = useStageState();

  // Keep the current page's thumbnail in view as the reader moves
  useEffect(() => {
    thumbs.reveal(currentPageIndex);
  }, [thumbs, currentPageIndex]);

  return (
    <Stage
      token={ThumbsToken}
      pageChrome={(page) => <span className="label">{page.pageIndex + 1}</span>}
    >
      {(page) => (
        <button
          onClick={() => main.goToPage(page.ref)}
          aria-current={page.pageIndex === currentPageIndex}
        >
          <RenderLayer />
        </button>
      )}
    </Stage>
  );
}

Loading live preview…

import { useEffect } from 'react';
import { Viewer, DocumentGate, createCapabilityToken } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import type { StageCapability } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

import './thumbnail-strip.css';

const engine = localEngine();

// The strip is a second view of the document, with its own token and settings.
const ThumbsToken = createCapabilityToken<StageCapability>('stage-thumbs');

const plugins = [
  stagePlugin(), // the main view
  stagePlugin({
    id: 'stage-thumbs',
    token: ThumbsToken,
    interaction: false, // a drag doesn't select text or draw
    zoomGestures: false, // a pinch doesn't resize the thumbnails
    zoom: { pageWidth: 96 },
    gap: { px: 12 },
    padding: 10,
    pageFrame: { bottom: 20 }, // room for the page number
    // A wide, short strip (on a phone) lines the thumbnails up in a row.
    responsive: [{ when: { orientation: 'landscape' }, settings: { layout: 'horizontal' } }],
  }),
  renderPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

function Thumbnails() {
  const main = useStage();
  const thumbs = useStage(ThumbsToken);
  const { currentPageIndex } = useStageState();

  // Keep the current page's thumbnail in view as the reader moves.
  useEffect(() => {
    thumbs.reveal(currentPageIndex);
  }, [thumbs, currentPageIndex]);

  return (
    <Stage
      token={ThumbsToken}
      className="strip"
      pageChrome={(page) => (
        <span className="number" style={{ height: page.frame.bottom }}>
          {page.pageIndex + 1}
        </span>
      )}
    >
      {(page) => (
        <button
          type="button"
          className="thumb"
          aria-label={`Page ${page.pageIndex + 1}`}
          aria-current={page.pageIndex === currentPageIndex}
          onClick={() => main.goToPage(page.ref)}
        >
          <RenderLayer />
        </button>
      )}
    </Stage>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <div className="reader">
          <Thumbnails />
          <Stage className="stage">{() => <RenderLayer />}</Stage>
        </div>
      </DocumentGate>
    </Viewer>
  );
}
  • Turning off interaction in the strip’s settings keeps a drag on it from selecting text in the document.
  • Turning off zoomGestures keeps the thumbnails their size under a pinch.
  • reveal() scrolls the strip only as far as needed, so it follows without jumping.

See Two views of one document for everything a second Stage can do.

Props#

PropWhat it isDefault
pageThe page, as its ref or its indexrequired
widthThe width of the page upright, in pixels: a page turned a quarter is that tall240
documentIdWhich documentthe one in scope, else the active one
fallbackWhat to show until the page is readynothing
pageFrameSpace reserved around the page for your own labels, in pixels per side0
pageChromeWhat to draw in that space—

fallback and pageChrome take content, and className and style go on the outer box.

Good to know#

How pages look#

A page here has the same background and shadow as on the Stage: the viewer’s page settings, or --epdf-page-background and --epdf-page-shadow from CSS (see Colors & theming).

A menu anchored to the page, such as the selection menu, is placed above everything else on the page, so a card or a scrolling list can’t cut it off.

Pointer input#

A <PageView> takes pointer input for the tools when the interaction plugin is registered, so text selection and annotation editing work in it too. On a touch screen, the Stage is the better choice: it has the gestures.

Was this page helpful?

Your feedback goes directly to the documentation team.