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.svelte
<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:

first-page.svelte
<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:

page-picker.svelte
<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.

thumbnail-strip-token.ts
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
  }),
];
thumbnail-strip.svelte
<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 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 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).

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.