---
title: "Your own annotation look — Angular"
description: "Draw annotations with your own components, react to hover, show cards next to them, and make them interactive."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/annotations/custom-rendering"
---

# Your own annotation look

The annotation layer draws each annotation the way the PDF describes it. You can draw some of them
with your own template instead: a note that looks like your app's comment bubble, a stamp
that shows live data from your server.

This changes the screen only. The PDF keeps the annotation's own look, so other PDF apps,
printing and downloads show the original:

| Where                               | What people see                 |
| :---------------------------------- | :------------------------------ |
| Your viewer                         | Your template                   |
| The saved PDF, other apps, printing | The annotation's own appearance |

## Its look, or UI on it

Ask whether it should zoom and turn with the PDF:

| You want                                                                            | Use                                            | It                                                                                       |
| :---------------------------------------------------------------------------------- | :--------------------------------------------- | :--------------------------------------------------------------------------------------- |
| A different look: a comment bubble instead of the note icon, a stamp with live data | A [renderer](#draw-an-annotation-your-way)     | Is part of the page: it zooms and turns with it, and sits under the selection handles    |
| A mark that stays on the annotation: a ✓, a status, a count of replies              | [Pinned anchored UI](#pin-ui-to-an-annotation) | Floats over the pages, upright and the same size at every zoom, exactly where you put it |
| A card or a menu next to it                                                         | [Anchored UI](#show-a-card-on-hover)           | Floats over the pages, and moves to stay in view                                         |

## Draw an annotation your way

Put a **renderer** in the layer for
each look: an `<ng-template epdfAnnotation>` that says which annotations it draws, and draws them:

**`renderers.ts`**

```typescript
import { Component } from '@angular/core';
import {
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  type Annotation,
} from '@embedpdf/angular/annotation';

@Component({
  selector: 'app-annotations',
  imports: [EpdfAnnotationLayer, EpdfAnnotationTemplate],
  template: `
    <epdf-annotation-layer>
      <ng-template [epdfAnnotation]="isNote" let-annotation>
        <!-- how this annotation looks: see below -->
      </ng-template>
    </epdf-annotation-layer>
  `,
})
export class Annotations {
  protected readonly isNote = (annotation: Annotation) => annotation.subtype === 'text';
}
```

The function you give `epdfAnnotation`
gets the annotation, with all its fields, so you can pick by kind, name, author or anything else.
A subtype picks every annotation of that kind: `epdfAnnotation="stamp"`. The first renderer that
matches wins; an annotation no renderer matches is drawn as usual.

Your template draws into a **frame**: a box the layer places, turns and scales exactly like
the annotation's own drawing, also while it's dragged, resized or turned. Fill it, with `width` and
`height` at `100%`, and draw at the annotation's 100% size: the layer scales your drawing with the
page, text and borders included, as it does the PDF's own:

**`bubble.ts`**

```typescript
import { Component } from '@angular/core';
import {
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  type Annotation,
} from '@embedpdf/angular/annotation';

@Component({
  selector: 'app-annotations',
  imports: [EpdfAnnotationLayer, EpdfAnnotationTemplate],
  template: `
    <epdf-annotation-layer>
      <ng-template [epdfAnnotation]="isNote" let-annotation let-hovered="hovered">
        <!-- It fills its frame (width and height 100% in your CSS), so the selection
             outline and the click area match it, and it turns with the annotation. -->
        <div class="bubble" [class.bubble--hover]="hovered">
          {{ annotation.author?.[0] ?? '?' }}
        </div>
      </ng-template>
    </epdf-annotation-layer>
  `,
})
export class Annotations {
  protected readonly isNote = (annotation: Annotation) => annotation.subtype === 'text';
}
```

The selection outline, the click area and the hover all use the frame, so a look that fills it
matches them. A note keeps its size on screen as people zoom in and stays upright when the page is
turned, and so does a look that fills its frame. To make the bubble bigger, make the note bigger: a
size in points is a third more in pixels at 100% zoom, so a 24-point note is 32 pixels.

Your template gets these:
the annotation is `let-annotation`, and the rest are named, such as `let-frame="frame"`.

| Prop          | What it is                                                                                                                                                                                                                                                             |
| :------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `annotation`  | The annotation, as `get()` returns it.                                                                                                                                                                                                                                 |
| `frame`       | The box you draw into: its `width` and `height` in pixels at the annotation's 100% size, its `rotation` on screen in degrees clockwise, and the `scale` the layer draws it at. Turn your content by `-rotation` to keep it upright, such as a label on a turned stamp. |
| `native`      | The layer's own drawing of it, filling the frame. Render it to keep the original look and add to it.                                                                                                                                                                   |
| `appearance`  | The engine's picture of the annotation as an image (`url`), or `null`. `native` already draws it in place.                                                                                                                                                             |
| `hovered`     | True while the pointer is over it.                                                                                                                                                                                                                                     |
| `selected`    | True while it's selected, such as to make room for the selection handles.                                                                                                                                                                                              |
| `interactive` | True when what you draw takes the pointer (see [below](#take-over-the-pointer)).                                                                                                                                                                                       |

**`renderers.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import {
  EpdfAnnotation,
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  withAnnotation,
  type Annotation,
} from '@embedpdf/angular/annotation';
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-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    EpdfAnnotationTemplate,
  ],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        identity: { userId: 'u_381', displayName: 'Dana Smith' },
        initialDocuments: [{ source: ebook }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './renderers.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <div class="segmented" role="group" aria-label="Look">
          <button type="button" [attr.aria-pressed]="mine()" (click)="mine.set(true)">
            Your look
          </button>
          <button type="button" [attr.aria-pressed]="!mine()" (click)="mine.set(false)">
            The PDF's look
          </button>
        </div>
        <!-- A note keeps its size on screen and stays upright: turn the view to see it. -->
        <button type="button" class="button" (click)="stage.rotateViewBy(90)">Turn the view</button>
      </div>
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer>
            @if (mine()) {
              <!-- A note drawn as your app's comment bubble, the author's initials in it. It
                   fills its frame, so the selection outline and the click area match it, and
                   it turns with the annotation: a note stays upright on a turned page. -->
              <ng-template [epdfAnnotation]="isNote" let-annotation let-hovered="hovered">
                <div class="bubble" [class.bubble--hover]="hovered">{{ initials(annotation) }}</div>
              </ng-template>
            }
          </epdf-annotation-layer>
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;

  protected readonly mine = signal(true);
  // Kept in a field, the same function on every page: the layer registers each look once.
  protected readonly isNote = (annotation: Annotation) => annotation.subtype === 'text';

  protected initials(annotation: Annotation) {
    return (annotation.author ?? '?')
      .split(' ')
      .map((word) => word[0])
      .join('');
  }

  constructor() {
    // On load: two notes on the cover, 24 points square: 32 pixels at 100%.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (this.annotation.status() !== 'ready' || !cover || this.added) return;
      this.added = true;
      untracked(() => {
        void this.annotation.create(cover, {
          subtype: 'text',
          rect: { x: 470, y: 228, width: 24, height: 24 },
          contents: 'Can we shorten the title?',
          color: '#facc15',
        });
        void this.annotation.create(cover, {
          subtype: 'text',
          rect: { x: 280, y: 516, width: 24, height: 24 },
          contents: 'Add the co-author',
          color: '#facc15',
        });
      });
    });
  }
}
```

**`renderers.css`**

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

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

/* One choice of a few: the pressed button is the current one */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 11px;
}

.segmented button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.segmented button:hover {
  color: var(--ink);
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(7 32 76 / 0.1);
}

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

/* A note as a comment bubble: the author's initials, filling its frame */
.bubble {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
  color: var(--surface);
  background: var(--bubble);
  border: 2px solid var(--surface);
  border-radius: 50% 50% 50% 4px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.3);
}

.bubble--hover {
  background: var(--bubble-hover);
}

/* Turns the view */
.button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 12px;
  cursor: pointer;
  white-space: nowrap;
}

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

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

/* The pages */
.stage {
  height: 420px;
  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);
}

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

> **Try it:** the notes are bubbles with their author's initials. Click one: the selection fits it.
> Press **Turn the view**: they stay upright, like the PDF's own notes. Switch to **The PDF's look**:
> that's what the saved PDF shows.

People can still select, move, resize and delete the annotation: your template draws, and the
viewer keeps handling the pointer. That's also why it gets no clicks of its own. For that, see
[Take over the pointer](#take-over-the-pointer).

## Show a card on hover

A hover card sits next to an annotation while the pointer is over it. It needs two things: the
annotation under the pointer, and an anchor that keeps a card attached to it:

**`hover-card.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfAnchored } from '@embedpdf/angular/anchored';
import { EpdfAnnotation } from '@embedpdf/angular/annotation';

// <app-stamp-card /> goes inside <epdf-stage>
@Component({
  selector: 'app-stamp-card',
  imports: [EpdfAnchored],
  template: `
    @if (hovered(); as stamp) {
      @if (stamp.subtype === 'stamp') {
        <epdf-anchored [anchor]="anchor()" placement="top">
          <div class="card">Approved by {{ stamp.author }}</div>
        </epdf-anchored>
      }
    }
  `,
})
export class StampCard {
  private readonly annotation = inject(EpdfAnnotation);

  protected readonly hovered = this.annotation.hovered; // the annotation under the pointer, or null
  protected readonly anchor = this.annotation.anchorOf(() => this.hovered()?.ref ?? null);
}
```

**`hover-card.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnchored } from '@embedpdf/angular/anchored';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
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 next to the annotation under the pointer: who wrote it, and what.
@Component({
  selector: 'demo-hover-card',
  imports: [EpdfAnchored],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (hovered(); as hovered) {
      @if (hovered.contents) {
        <epdf-anchored [anchor]="anchor()" placement="top">
          <div class="card">
            <strong>{{ hovered.author ?? 'Someone' }}</strong>
            <p>{{ hovered.contents }}</p>
          </div>
        </epdf-anchored>
      }
    }
  `,
})
export class HoverCard {
  private readonly annotation = inject(EpdfAnnotation);

  protected readonly hovered = this.annotation.hovered; // the annotation under the pointer, or null
  protected readonly anchor = this.annotation.anchorOf(() => this.hovered()?.ref ?? null);
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    HoverCard,
  ],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        identity: { userId: 'u_381', displayName: 'Dana Smith' },
        initialDocuments: [{ source: ebook }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './hover-card.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <p class="hint">Point at a note or the green rectangle</p>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
        <demo-hover-card />
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;

  constructor() {
    // On load: two notes and a rectangle on the cover, each with a comment.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (this.annotation.status() !== 'ready' || !cover || this.added) return;
      this.added = true;
      untracked(() => {
        void this.annotation.create(cover, {
          subtype: 'text',
          rect: { x: 470, y: 232, width: 20, height: 20 },
          contents: 'Can we shorten the title?',
          color: '#facc15',
        });
        void this.annotation.create(cover, {
          subtype: 'text',
          rect: { x: 280, y: 520, width: 20, height: 20 },
          contents: 'Add the co-author',
          color: '#facc15',
        });
        void this.annotation.create(cover, {
          subtype: 'square',
          box: { x: 96, y: 376, width: 360, height: 118 },
          contents: 'This subtitle reads well',
          color: '#30a46c',
          strokeWidth: 3,
        });
      });
    });
  }
}
```

**`hover-card.css`**

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

/* The card next to the annotation under the pointer */
.card {
  max-width: 220px;
  padding: 10px 12px;
  font-size: 13px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 10px 28px -10px rgb(7 32 76 / 0.3);
}

.card strong {
  color: var(--ink);
}

.card p {
  margin: 4px 0 0;
}

.hint {
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  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);
}

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

Put it inside
`<epdf-stage>`, like a menu. The card stays next to the annotation while the user scrolls and
zooms, and while the annotation is moved.

`annotation.anchorOf(ref)` works for any annotation, not only the hovered one: see the next section.

## Pin UI to an annotation

A ✓ on an approved stamp, a status under it, a count of replies next to a note: UI that belongs to
one annotation and stays on it. `pinned` keeps it exactly where you put it. It doesn't flip or move to
stay in view, and it scrolls away with the annotation:

**`pinned.ts`**

```typescript
import { Component, inject, input } from '@angular/core';
import { EpdfAnchored } from '@embedpdf/angular/anchored';
import { annotationKey, type Annotation, EpdfAnnotation } from '@embedpdf/angular/annotation';
import { Approvals } from './approvals'; // your own data

// Pinned under the stamp: it stays there while the stamp moves, and scrolls away with it.
@Component({
  selector: 'app-approval-status',
  imports: [EpdfAnchored],
  template: `
    <epdf-anchored [anchor]="anchor()" placement="bottom" pinned>
      <div class="status">
        {{ approval().signedOff ? 'Signed off' : 'Waiting' }}
        <button (click)="approval().toggle()">
          {{ approval().signedOff ? 'Undo' : 'Sign off' }}
        </button>
      </div>
    </epdf-anchored>
  `,
})
export class ApprovalStatus {
  readonly stamp = input.required<Annotation>();

  protected readonly anchor = inject(EpdfAnnotation).anchorOf(() => this.stamp().ref);
  protected readonly approval = inject(Approvals).of(() => annotationKey(this.stamp().ref)); // your own data
}
```

Put one for each annotation that needs it inside `<epdf-stage>`,
from `annotation.watch()`. Anchored UI on a page that isn't on screen isn't drawn, so a document
with hundreds of them costs only the ones in view.

**`stamp-status.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  output,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnchored } from '@embedpdf/angular/anchored';
import {
  EpdfAnnotation,
  EpdfAnnotationLayer,
  withAnnotation,
  type Annotation,
} from '@embedpdf/angular/annotation';
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()) };
};

// One stamp's status, pinned to it: a check on its corner once it's signed off,
// and a status with a button under it. Both stay there while the stamp moves.
@Component({
  selector: 'demo-approval-status',
  imports: [EpdfAnchored],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (signedOff()) {
      <epdf-anchored [anchor]="anchor()" placement="top-end" [gap]="-12" pinned>
        <span class="check">✓</span>
      </epdf-anchored>
    }
    <epdf-anchored [anchor]="anchor()" placement="bottom" [gap]="8" pinned>
      <div class="status">
        <span class="dot" [class.dot--done]="signedOff()"></span>
        {{ signedOff() ? 'Signed off' : 'Waiting' }}
        <button type="button" class="sign" [class.sign--undo]="signedOff()" (click)="toggle.emit()">
          {{ signedOff() ? 'Undo' : 'Sign off' }}
        </button>
      </div>
    </epdf-anchored>
  `,
})
export class ApprovalStatus {
  readonly stamp = input.required<Annotation>();
  readonly signedOff = input.required<boolean>();
  readonly toggle = output();

  protected readonly anchor = inject(EpdfAnnotation).anchorOf(() => this.stamp().ref);
}

// An approval stamp's name, which the stamp keeps in every PDF app; null for any other annotation.
const approvalName = (annotation: Annotation): string | null =>
  annotation.subtype === 'stamp' && annotation.name?.startsWith('approval-')
    ? annotation.name
    : null;

// Every approval stamp's status, inside <epdf-stage>. Your own data for each
// stamp is kept by its name.
@Component({
  selector: 'demo-approvals',
  imports: [ApprovalStatus],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (approval of approvals(); track approval.name) {
      <demo-approval-status
        [stamp]="approval.stamp"
        [signedOff]="signedOff()[approval.name] ?? false"
        (toggle)="toggle(approval.name)"
      />
    }
  `,
})
export class Approvals {
  private readonly stamps = inject(EpdfAnnotation).watch({ subtype: 'stamp' });
  protected readonly signedOff = signal<Partial<Record<string, boolean>>>({
    'approval-budget': true,
  });

  protected readonly approvals = computed(() =>
    this.stamps().flatMap((stamp) => {
      const name = approvalName(stamp);
      return name ? [{ name, stamp }] : [];
    }),
  );

  protected toggle(name: string) {
    this.signedOff.update((current) => ({ ...current, [name]: !current[name] }));
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    Approvals,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './stamp-status.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <p class="hint">Sign a stamp off, then move it: its status goes with it.</p>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
        <demo-approvals />
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;

  constructor() {
    // On load: two approval stamps on the cover, one signed off. The second sits
    // at the bottom, so its status hangs over the next page.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (this.annotation.status() !== 'ready' || !cover || this.added) return;
      this.added = true;
      untracked(() => {
        void approvedPicture().then(async (appearance) => {
          await this.annotation.create(
            cover,
            {
              subtype: 'stamp',
              box: { x: 330, y: 470, width: 180, height: 60 },
              name: 'approval-budget',
            },
            { appearance },
          );
          await this.annotation.create(
            cover,
            {
              subtype: 'stamp',
              box: { x: 96, y: 712, width: 180, height: 60 },
              name: 'approval-contract',
            },
            { appearance },
          );
        });
      });
    });
  }
}

// The stamp's picture, drawn on a canvas so the example needs no image file.
function approvedPicture(): Promise<Blob> {
  const canvas = document.createElement('canvas');
  canvas.width = 360;
  canvas.height = 120;
  const context = canvas.getContext('2d')!;
  context.strokeStyle = '#30a46c';
  context.lineWidth = 10;
  context.strokeRect(5, 5, 350, 110);
  context.fillStyle = '#30a46c';
  context.font = 'bold 54px sans-serif';
  context.textAlign = 'center';
  context.textBaseline = 'middle';
  context.fillText('APPROVED', 180, 64);
  return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob!), 'image/png'));
}
```

**`stamp-status.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
  --waiting: #f5a524;
  --done: #30a46c;
}

/* What to try, above the pages */
.hint {
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--muted);
}

/* Your status under a stamp, and its button */
.status {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 5px 5px 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: 0 6px 18px rgb(7 32 76 / 0.22);
}

.dot {
  width: 9px;
  height: 9px;
  background: var(--waiting);
  border-radius: 50%;
}

.dot--done {
  background: var(--done);
}

.sign {
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--surface);
  background: var(--done);
  border: 0;
  border-radius: 999px;
  padding: 4px 10px;
  cursor: pointer;
}

.sign--undo {
  color: var(--ink);
  background: var(--tint);
}

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

/* The check on a signed-off stamp's corner */
.check {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  font-size: 13px;
  font-weight: 700;
  color: var(--surface);
  background: var(--done);
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.3);
}

/* The pages */
.stage {
  height: 420px;
  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);
}

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

> **Try it:** sign the second stamp off, then select a stamp and move it: its status goes with it. The
> second stamp's status hangs over the next page, and stays on top.

1. The stamps are placed like any stamp, so the PDF holds the real ones; see [Stamps](stamps).
2. Each one is picked out by its `name`, which the stamp keeps in every PDF app.
3. The status sits under the stamp with `placement="bottom"`, and the ✓ on its corner with
   `placement="top-end"` and a negative `gap`. See [Where it goes](../ui/anchored#where-it-goes).
4. The stamp keeps its own look and stays selectable: the buttons are next to it, not in it.

To find your own data for an annotation, key it by `annotationKey(annotation.ref)`, which stays the
same after the document is saved and opened again, or by its `name` when you gave it one.

## Take over the pointer

Sometimes your template must take clicks itself: buttons, links, a text field. Add `epdfAnnotationInteractive` to the renderer:

**`interactive.ts`**

```typescript
import { Component } from '@angular/core';
import {
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  type Annotation,
} from '@embedpdf/angular/annotation';
import { ApprovalWidget } from './approval-widget';

@Component({
  selector: 'app-annotations',
  imports: [EpdfAnnotationLayer, EpdfAnnotationTemplate, ApprovalWidget],
  template: `
    <epdf-annotation-layer>
      <ng-template [epdfAnnotation]="isApproved" epdfAnnotationInteractive let-annotation>
        <app-approval-widget [annotation]="annotation" />
      </ng-template>
    </epdf-annotation-layer>
  `,
})
export class Annotations {
  protected readonly isApproved = (annotation: Annotation) =>
    annotation.subtype === 'stamp' && annotation.name === 'Approved';
}
```

This is for when the annotation itself is the control, such as a stamp you tick like a checkbox. For
buttons next to an annotation, [pin them](#pin-ui-to-an-annotation) instead: the annotation stays
selectable.

While a renderer is interactive, the pointer belongs to your template, and people can't select,
move or delete that annotation with it. Most apps want both: interactive while reading, editable
while editing. Pass a function, and the viewer asks it whenever it matters:

**`interactive-when.ts`**

```typescript
import { Component } from '@angular/core';
import {
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  type Annotation,
} from '@embedpdf/angular/annotation';
import { ApprovalWidget } from './approval-widget';

@Component({
  selector: 'app-annotations',
  imports: [EpdfAnnotationLayer, EpdfAnnotationTemplate, ApprovalWidget],
  template: `
    <epdf-annotation-layer>
      <ng-template
        [epdfAnnotation]="isApproved"
        [epdfAnnotationInteractive]="inReadingMode"
        let-annotation
      >
        <app-approval-widget [annotation]="annotation" />
      </ng-template>
    </epdf-annotation-layer>
  `,
})
export class Annotations {
  protected readonly isApproved = (annotation: Annotation) =>
    annotation.subtype === 'stamp' && annotation.name === 'Approved';

  // interactive in reading mode only
  protected readonly inReadingMode = ({ toolId }: { toolId: string }) => toolId === 'pan';
}
```

## Good to know

### Drawing at the size on screen

A renderer with `scale: false` draws at the frame's size on screen instead, and sizes its text and
borders itself from `frame.scale`. Use it for something that must lay itself out at full resolution,
such as a canvas.

### Hover cards people can use

A card closes when the pointer leaves the annotation, so the pointer can't reach a button in it.
Keep the card open while the pointer is over the card itself, for example with a short delay
before it closes and a `pointerenter` on the card that cancels it.

### Form fields

A form field's widgets aren't annotations: they're the form's, and the `<epdf-form-layer>` draws them, so
your renderers never get them. See [Forms and annotations](../forms#forms-and-annotations).

### Other layers

A renderer replaces the drawing inside the annotation layer. Anything that isn't tied to one
annotation, such as a comments sidebar, belongs in your own UI, fed by `annotation.watch()`.
