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:
<script lang="ts">
import { PageView } from '@embedpdf/svelte/page-view';
import { RenderLayer } from '@embedpdf/svelte/render';
import type { PageRef } from '@embedpdf/svelte/runtime';
let { page }: { page: PageRef } = $props();
</script>
<PageView {page} width={240}>
<RenderLayer />
</PageView>Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { renderPlugin } from '@embedpdf/svelte/render';
import { localEngine } from '@embedpdf/engine';
import PageCard from './PageCard.svelte';
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()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<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:
<script lang="ts">
import { PageView } from '@embedpdf/svelte/page-view';
import { RenderLayer } from '@embedpdf/svelte/render';
</script>
<PageView page={0} width={320}>
<RenderLayer />
</PageView>usePageList() gives you every page of the document, in order, to loop over:
<script lang="ts">
import { PageView } from '@embedpdf/svelte/page-view';
import { RenderLayer } from '@embedpdf/svelte/render';
import { usePageList, type PageRef } from '@embedpdf/svelte/runtime';
let { onPick }: { onPick: (page: PageRef) => void } = $props();
const pages = usePageList();
</script>
{#each pages.current as page (page.index)}
<button onclick={() => onPick(page.ref)}>
<PageView page={page.ref} width={120}>
<RenderLayer />
</PageView>
{page.label ?? page.index + 1}
</button>
{/each}Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { renderPlugin } from '@embedpdf/svelte/render';
import { localEngine } from '@embedpdf/engine';
import PagePicker from './PagePicker.svelte';
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()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<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/svelte/runtime';
import { stagePlugin, type StageCapability } from '@embedpdf/svelte/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
}),
];<script lang="ts">
import { RenderLayer } from '@embedpdf/svelte/render';
import { Stage, useStage, useStageState } from '@embedpdf/svelte/stage';
import { ThumbsToken } from './thumbnail-strip-token';
const main = useStage();
const thumbs = useStage(ThumbsToken);
const state = useStageState();
// Keep the current page's thumbnail in view as the reader moves
$effect(() => {
thumbs.reveal(state.currentPageIndex);
});
</script>
<Stage token={ThumbsToken}>
{#snippet children(page)}
<button
onclick={() => main.goToPage(page.ref)}
aria-current={page.pageIndex === state.currentPageIndex}
>
<RenderLayer />
</button>
{/snippet}
{#snippet pageChrome(page)}
<span class="label">{page.pageIndex + 1}</span>
{/snippet}
</Stage>Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { localEngine } from '@embedpdf/engine';
import Thumbnails from './Thumbnails.svelte';
import { ThumbsToken } from './thumbs-token';
import '../thumbnail-strip.css';
const engine = localEngine();
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()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<div class="reader">
<Thumbnails />
<Stage class="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 are snippets, and class 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.