EmbedPDF

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:

page-preview.ts
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:

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

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

thumbnail-strip-token.ts
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 {}
thumbnail-strip.ts
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 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 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).

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.

Was this page helpful?

Your feedback goes directly to the documentation team.