---
title: "Single pages & thumbnails — Angular"
description: "Show one page on its own, anywhere in your app, and build a thumbnail strip that follows the reader."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/viewing/page-view"
---

# 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`**

```typescript
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>();
}
```

**`basic.ts`**

```typescript
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 {}
```

**`basic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* A card with one page in it: side by side, stacked on a narrow screen */
.card {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 18px;
  margin: 0;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

/* The page is plain DOM: give it a place like any other element */
.page {
  flex: none;
}

.caption {
  flex: 1 1 180px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.title {
  font-size: 16px;
  color: var(--ink);
}

.detail {
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.pager {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.button {
  min-width: 38px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 11px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

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`**

```typescript
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`**

```typescript
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>();
}
```

**`picker.ts`**

```typescript
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 {}
```

**`picker.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The pages to pick from beside the picked one; above it on a narrow screen */
.picker {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.choices {
  flex: 1 1 100%;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 12px;
  align-content: start;
}

.choice {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 8px;
  padding: 6px 0;
  cursor: pointer;
}

.choice:hover {
  background: var(--tint);
}

.choice[aria-selected='true'] {
  color: var(--accent);
  background: var(--tint);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.choice:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.number {
  font-variant-numeric: tabular-nums;
}

.shown {
  flex: 1 1 100%;
  display: flex;
  justify-content: center;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
}

@container (min-width: 600px) {
  .choices {
    flex: 1 1 0;
  }

  .shown {
    flex: 0 0 auto;
  }
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

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`**

```typescript
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`**

```typescript
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;
}
```

**`thumbnail-strip.ts`**

```typescript
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;
}
```

**`thumbnail-strip.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The strip beside the pages; a row above them on a narrow screen */
.reader {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.strip,
.stage {
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.strip {
  flex: 1 1 100%;
  height: 156px;
}

.stage {
  flex: 1 1 100%;
  height: 420px;
}

@container (min-width: 560px) {
  .strip {
    flex: 0 0 140px;
    height: 420px;
  }

  .stage {
    flex: 1 1 0;
    min-width: 0;
  }
}

/* A thumbnail is a button over the page picture */
.thumb {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.thumb[aria-current='true'] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.thumb:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The page number, in the band reserved below each thumbnail */
.number {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

- **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](stage#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](../ui/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.
