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:
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:
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:
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.
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
}),
];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
interactionin the strip’s settings keeps a drag on it from selecting text in the document. - Turning off
zoomGestureskeeps 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#
| Prop | What it is | Default |
|---|---|---|
page | The page, as its ref or its index | required |
width | The width of the page upright, in pixels: a page turned a quarter is that tall | 240 |
documentId | Which document | the one in scope, else the active one |
fallback | What to show until the page is ready | nothing |
pageFrame | Space reserved around the page for your own labels, in pixels per side | 0 |
pageChrome | What 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).
Menus inside a page view#
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.
Your feedback goes directly to the documentation team.