Single pages & thumbnails
<epdf-page-view> 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 { Component, input } from '@angular/core';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import type { PageRef } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-page-preview',
imports: [EpdfPageView, EpdfRenderLayer],
template: `
<epdf-page-view [page]="page()" [width]="240">
<epdf-render-layer />
</epdf-page-view>
`,
})
export class PagePreview {
readonly page = input.required<PageRef>();
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';
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.
@Component({
selector: 'demo-page-card',
imports: [EpdfPageView, EpdfRenderLayer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<figure class="card">
<epdf-page-view [page]="index()" [width]="220" class="page">
<epdf-render-layer />
</epdf-page-view>
<figcaption class="caption">
<strong class="title">Page {{ page()?.label ?? index() + 1 }}</strong>
@if (page(); as page) {
<span class="detail">
{{ round(page.size.width) }} × {{ round(page.size.height) }} points
</span>
}
<span class="pager">
<button
type="button"
class="button"
[disabled]="index() === 0"
(click)="index.set(index() - 1)"
>
‹ Previous
</button>
<button
type="button"
class="button"
[disabled]="index() >= pages().length - 1"
(click)="index.set(index() + 1)"
>
Next ›
</button>
</span>
</figcaption>
</figure>
`,
})
export class PageCard {
protected readonly pages = inject(EpdfDocument).pages;
protected readonly index = signal(0);
protected readonly page = computed(() => this.pages()[this.index()]);
protected readonly round = Math.round;
}
// No Stage: a page on its own needs only the render plugin.
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, PageCard],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './basic.css',
encapsulation: ViewEncapsulation.None,
template: `
<demo-page-card *epdfDocumentGate="let document; fallback: loading" />
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {}Every layer works in <epdf-page-view> 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 { Component } from '@angular/core';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
@Component({
selector: 'app-first-page',
imports: [EpdfPageView, EpdfRenderLayer],
template: `
<epdf-page-view [page]="0" [width]="320">
<epdf-render-layer />
</epdf-page-view>
`,
})
export class FirstPage {}document.pages() gives you every page of the document, in order, to loop over:
import { Component, inject, output } from '@angular/core';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfDocument, type PageRef } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-page-picker',
imports: [EpdfPageView, EpdfRenderLayer],
template: `
@for (page of document.pages(); track page.index) {
<button (click)="pick.emit(page.ref)">
<epdf-page-view [page]="page.ref" [width]="120">
<epdf-render-layer />
</epdf-page-view>
{{ page.label ?? page.index + 1 }}
</button>
}
`,
})
export class PagePicker {
protected readonly document = inject(EpdfDocument);
readonly pick = output<PageRef>();
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';
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()) };
};
@Component({
selector: 'demo-page-picker',
imports: [EpdfPageView, EpdfRenderLayer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="picker">
<div class="choices" role="listbox" aria-label="Pages">
@for (page of document.pages(); track page.ref.objectNumber) {
<button
type="button"
role="option"
class="choice"
[attr.aria-selected]="page.ref.objectNumber === shown()?.objectNumber"
(click)="picked.set(page.ref)"
>
<epdf-page-view [page]="page.ref" [width]="84">
<epdf-render-layer />
</epdf-page-view>
<span class="number">{{ page.label ?? page.index + 1 }}</span>
</button>
}
</div>
<div class="shown">
@if (shown(); as shown) {
<epdf-page-view [page]="shown" [width]="300">
<epdf-render-layer />
</epdf-page-view>
}
</div>
</div>
`,
})
export class PagePicker {
protected readonly document = inject(EpdfDocument);
protected readonly picked = signal<PageRef | null>(null);
// A ref follows its page when pages move, so it's what you keep.
protected readonly shown = computed(() => this.picked() ?? this.document.pages()[0]?.ref ?? null);
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, PagePicker],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './picker.css',
encapsulation: ViewEncapsulation.None,
template: `
<demo-page-picker *epdfDocumentGate="let document; fallback: loading" />
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {}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
<epdf-page-view>s: it mounts only the thumbnails you can see, and it can follow the page the reader is on.
import { Component } from '@angular/core';
import { createCapabilityToken, provideEmbedPdf } from '@embedpdf/angular/runtime';
import { withStage, type StageCapability } from '@embedpdf/angular/stage';
import { engine } from './pdf';
export const ThumbsToken = createCapabilityToken<StageCapability>('stage-thumbs');
@Component({
selector: 'app-reader',
providers: [
provideEmbedPdf(
{ engine },
withStage(),
withStage({
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
}),
),
],
template: `<!-- … -->`,
})
export class Reader {}import { Component } from '@angular/core';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageChrome, EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { ThumbsToken } from './thumbnail-strip-token';
@Component({
selector: 'app-reader',
imports: [EpdfStage, EpdfPageTemplate, EpdfPageChrome, EpdfRenderLayer],
template: `
<!-- The main view. As the reader moves, keep the current page's thumbnail in view -->
<epdf-stage #main="epdfStage" (pageChange)="thumbs.reveal($event)">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<!-- The thumbnail strip: a second stage, with its own reference -->
<epdf-stage #thumbs="epdfStage" [token]="thumbsToken">
<ng-template epdfPage let-page>
<button
(click)="main.goToPage(page.ref)"
[attr.aria-current]="page.pageIndex() === main.currentPageIndex()"
>
<epdf-render-layer />
</button>
</ng-template>
<ng-template epdfPageChrome let-page>
<span class="label">{{ page.pageIndex() + 1 }}</span>
</ng-template>
</epdf-stage>
`,
})
export class Reader {
protected readonly thumbsToken = ThumbsToken;
}Loading live preview…
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import {
createCapabilityToken,
EpdfDocumentGate,
provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageChrome, EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import type { StageCapability } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';
// The strip is a second view of the document, with its own token and settings.
const ThumbsToken = createCapabilityToken<StageCapability>('stage-thumbs');
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()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfPageChrome, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(), // the main view
withStage({
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' } }],
}),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './thumbnail-strip.css',
encapsulation: ViewEncapsulation.None,
template: `
<div *epdfDocumentGate="let document; fallback: loading" class="reader">
<epdf-stage #thumbs="epdfStage" [token]="thumbsToken" class="strip">
<ng-template epdfPage let-page>
<button
type="button"
class="thumb"
[attr.aria-label]="'Page ' + (page.pageIndex() + 1)"
[attr.aria-current]="page.pageIndex() === main.currentPageIndex()"
(click)="main.goToPage(page.ref)"
>
<epdf-render-layer />
</button>
</ng-template>
<ng-template epdfPageChrome let-page>
<span class="number" [style.height.px]="page.frame().bottom">
{{ page.pageIndex() + 1 }}
</span>
</ng-template>
</epdf-stage>
<!-- Keep the current page's thumbnail in view as the reader moves. -->
<epdf-stage #main="epdfStage" class="stage" (pageChange)="thumbs.reveal($event)">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
</div>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly thumbsToken = ThumbsToken;
}- 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 templates, <ng-template epdfFallback> and
<ng-template epdfPageChrome>, 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 <epdf-page-view> 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.