---
title: "Getting Started — Angular"
description: "Build your own PDF viewer UI with the EmbedPDF headless libraries."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/getting-started"
---

# Getting Started

The headless libraries give you the engine and the plugin system — you own
every pixel of the UI. An app depends on two packages: the adapter for your
framework, and an engine.

## Installation

```sh
pnpm add @embedpdf/angular @embedpdf/engine
```

The Angular adapter ships each vertical as a secondary entry point —
Angular's native library modularity — with inject functions instead of
hooks.

## Your first viewer

`localEngine()` creates the engine — synchronously, allocating nothing until
first use, so a module-scope `const engine = localEngine()` is safe (even under
SSR). Hand it to `<Viewer>`: the viewer warms it up on mount and PDFium boots
in a worker in the background, so the stage and render layer draw the pages the
moment a document is ready — no worker wiring, no lifecycle to manage. This is
the whole app:

**`basic.ts`**

```typescript
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { localEngine } from '@embedpdf/engine';
import { EpdfViewer, injectDocumentId } from '@embedpdf/angular/runtime';
import type { EpdfInitialDocument, OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, stagePlugin } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, renderPlugin } from '@embedpdf/angular/render';

// The local engine opens bytes: fetch lazily, under the loading tab.
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()) };
};

// Kernel readers live INSIDE <epdf-viewer>, where the host is injectable —
// and document UI is gated on having a document.
@Component({
  selector: 'demo-workspace',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
  template: `
    @if (documentId()) {
      <epdf-stage style="display: block; height: 100%">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    } @else {
      <p>Loading…</p>
    }
  `,
})
export class Workspace {
  readonly documentId = injectDocumentId();
}

@Component({
  selector: 'demo-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [EpdfViewer, Workspace],
  template: `
    <epdf-viewer [engine]="engine" [plugins]="plugins" [initialDocuments]="initialDocuments">
      <div style="height: 500px">
        <demo-workspace />
      </div>
    </epdf-viewer>
  `,
})
export class App {
  // `localEngine()` IS the engine — created synchronously, costing nothing
  // until first use. The viewer warms it up when the kernel materializes;
  // PDFium boots in a worker in the background, and only opening a document
  // awaits it — the UI renders at t≈0.
  readonly engine = localEngine();
  readonly plugins = [stagePlugin(), renderPlugin()];
  readonly initialDocuments: EpdfInitialDocument[] = [{ source: ebook }];
}
```

Prefer the viewer to own the engine's lifetime — created on mount, destroyed on
unmount? Pass a thunk instead: `engine={() => localEngine()}`. See the
[Engine getting started](https://www.embedpdf.com/docs/engine/getting-started) for the ownership model,
fallback fonts, cloud, and SSR.
