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.vue
<script setup lang="ts">
import { PageView } from '@embedpdf/vue/page-view';
import { RenderLayer } from '@embedpdf/vue/render';
import type { PageRef } from '@embedpdf/vue/runtime';

defineProps<{ page: PageRef }>();
</script>

<template>
  <PageView :page="page" :width="240">
    <RenderLayer />
  </PageView>
</template>

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import PageCard from './PageCard.vue';

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <PageCard />
    </DocumentGate>
  </Viewer>
</template>

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.vue
<script setup lang="ts">
import { PageView } from '@embedpdf/vue/page-view';
import { RenderLayer } from '@embedpdf/vue/render';
</script>

<template>
  <PageView :page="0" :width="320">
    <RenderLayer />
  </PageView>
</template>

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

page-picker.vue
<script setup lang="ts">
import { PageView } from '@embedpdf/vue/page-view';
import { RenderLayer } from '@embedpdf/vue/render';
import { usePageList, type PageRef } from '@embedpdf/vue/runtime';

const emit = defineEmits<{ pick: [page: PageRef] }>();
const pages = usePageList();
</script>

<template>
  <button v-for="page in pages" :key="page.index" @click="emit('pick', page.ref)">
    <PageView :page="page.ref" :width="120">
      <RenderLayer />
    </PageView>
    {{ page.label ?? page.index + 1 }}
  </button>
</template>

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import PagePicker from './PagePicker.vue';

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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <PagePicker />
    </DocumentGate>
  </Viewer>
</template>

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/vue/runtime';
import { stagePlugin, type StageCapability } from '@embedpdf/vue/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.vue
<script setup lang="ts">
import { watch } from 'vue';
import { RenderLayer } from '@embedpdf/vue/render';
import { Stage, useStage, useStageState } from '@embedpdf/vue/stage';
import { ThumbsToken } from './thumbnail-strip-token';

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

// Keep the current page's thumbnail in view as the reader moves
watch(currentPageIndex, (index) => thumbs.reveal(index), { immediate: true });
</script>

<template>
  <Stage :token="ThumbsToken">
    <template #page="{ page }">
      <button
        :aria-current="page.pageIndex === currentPageIndex"
        @click="main.goToPage(page.ref)"
      >
        <RenderLayer />
      </button>
    </template>
    <template #page-chrome="{ page }">
      <span class="label">{{ page.pageIndex + 1 }}</span>
    </template>
  </Stage>
</template>

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { localEngine } from '@embedpdf/engine';
import Thumbnails from './Thumbnails.vue';
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>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <div class="reader">
        <Thumbnails />
        <Stage class="stage">
          <template #page><RenderLayer /></template>
        </Stage>
      </div>
    </DocumentGate>
  </Viewer>
</template>
  • 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
document-idWhich documentthe one in scope, else the active one
fallbackWhat to show until the page is readynothing
page-frameSpace reserved around the page for your own labels, in pixels per side0
page-chromeWhat to draw in that space—

fallback and page-chrome are slots, <template #fallback> and <template #page-chrome>, 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.