---
title: "Your own annotation look — React"
description: "Draw annotations with your own components, react to hover, show cards next to them, and make them interactive."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/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 component 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 component                  |
| 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

Give the layer a list of **renderers**. Each one says which
annotations it draws and with which component:

**`renderers.tsx`**

```tsx
import { AnnotationLayer, type AnnotationRenderer } from '@embedpdf/react/annotation';
import { CommentBubble } from './comment-bubble';

const renderers: AnnotationRenderer[] = [
  { for: (annotation) => annotation.subtype === 'text', component: CommentBubble },
];

export const Annotations = () => <AnnotationLayer renderers={renderers} />;
```

`for`
gets the annotation, with all its fields, so you can pick by kind, name, author or anything else. The first renderer that
matches wins; an annotation no renderer matches is drawn as usual.

Define the list once, outside your component or in `useMemo`. The layer registers each entry, and
a new list every render would register them again (the viewer warns about it while you develop).

Your component 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.tsx`**

```tsx
import type { AnnotationRendererProps } from '@embedpdf/react/annotation';

// 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.
export function CommentBubble({ annotation, hovered }: AnnotationRendererProps) {
  return (
    <div className={hovered ? 'bubble bubble--hover' : 'bubble'}>
      {annotation.author?.[0] ?? '?'}
    </div>
  );
}
```

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 component gets:

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

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationState,
  type AnnotationRenderer,
  type AnnotationRendererProps,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './renderers.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

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 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.
function CommentBubble({ annotation, hovered }: AnnotationRendererProps) {
  const initials = (annotation.author ?? '?')
    .split(' ')
    .map((word) => word[0])
    .join('');

  return <div className={hovered ? 'bubble bubble--hover' : 'bubble'}>{initials}</div>;
}

// Defined once, outside the component: the layer registers each entry.
const RENDERERS: AnnotationRenderer[] = [
  { for: (annotation) => annotation.subtype === 'text', component: CommentBubble },
];
const NONE: AnnotationRenderer[] = [];

// On load: two notes on the cover, 24 points square: 32 pixels at 100%.
function AddNotes() {
  const annotation = useAnnotation();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const cover = usePageList()[0]?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (!ready || !cover || added.current) return;
    added.current = true;
    void annotation.create(cover, {
      subtype: 'text',
      rect: { x: 470, y: 228, width: 24, height: 24 },
      contents: 'Can we shorten the title?',
      color: '#facc15',
    });
    void annotation.create(cover, {
      subtype: 'text',
      rect: { x: 280, y: 516, width: 24, height: 24 },
      contents: 'Add the co-author',
      color: '#facc15',
    });
  }, [annotation, ready, cover]);

  return null;
}

// A note keeps its size on screen and stays upright: turn the view to see it.
function TurnButton() {
  const stage = useStage();
  return (
    <button type="button" className="button" onClick={() => stage.rotateViewBy(90)}>
      Turn the view
    </button>
  );
}

export default function App() {
  const [mine, setMine] = useState(true);

  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      identity={{ userId: 'u_381', displayName: 'Dana Smith' }}
      initialDocuments={[{ source: ebook }]}
    >
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddNotes />
        <div className="toolbar">
          <div className="segmented" role="group" aria-label="Look">
            <button type="button" aria-pressed={mine} onClick={() => setMine(true)}>
              Your look
            </button>
            <button type="button" aria-pressed={!mine} onClick={() => setMine(false)}>
              The PDF’s look
            </button>
          </div>
          <TurnButton />
        </div>
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer renderers={mine ? RENDERERS : NONE} />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`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 component 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.tsx`**

```tsx
import { Anchored } from '@embedpdf/react/anchored';
import {
  AnnotationLayer,
  useAnnotationAnchor,
  useAnnotationState,
} from '@embedpdf/react/annotation';
import { Stage } from '@embedpdf/react/stage';

function StampCard() {
  const { hovered } = useAnnotationState(); // the annotation under the pointer, or null
  const anchor = useAnnotationAnchor(hovered?.ref ?? null);

  if (hovered?.subtype !== 'stamp') return null;
  return (
    <Anchored anchor={anchor} placement="top">
      <div className="card">Approved by {hovered.author}</div>
    </Anchored>
  );
}

export const Pages = () => <Stage overlay={<StampCard />}>{() => <AnnotationLayer />}</Stage>;
```

**`hover-card.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { Anchored } from '@embedpdf/react/anchored';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationAnchor,
  useAnnotationState,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './hover-card.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

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

// On load: two notes and a rectangle on the cover, each with a comment.
function AddAnnotations() {
  const annotation = useAnnotation();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const cover = usePageList()[0]?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (!ready || !cover || added.current) return;
    added.current = true;
    void annotation.create(cover, {
      subtype: 'text',
      rect: { x: 470, y: 232, width: 20, height: 20 },
      contents: 'Can we shorten the title?',
      color: '#facc15',
    });
    void annotation.create(cover, {
      subtype: 'text',
      rect: { x: 280, y: 520, width: 20, height: 20 },
      contents: 'Add the co-author',
      color: '#facc15',
    });
    void annotation.create(cover, {
      subtype: 'square',
      box: { x: 96, y: 376, width: 360, height: 118 },
      contents: 'This subtitle reads well',
      color: '#30a46c',
      strokeWidth: 3,
    });
  }, [annotation, ready, cover]);

  return null;
}

// A card next to the annotation under the pointer: who wrote it, and what.
function HoverCard() {
  const { hovered } = useAnnotationState(); // the annotation under the pointer, or null
  const anchor = useAnnotationAnchor(hovered?.ref ?? null);

  if (!hovered?.contents) return null;
  return (
    <Anchored anchor={anchor} placement="top">
      <div className="card">
        <strong>{hovered.author ?? 'Someone'}</strong>
        <p>{hovered.contents}</p>
      </div>
    </Anchored>
  );
}

export default function App() {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      identity={{ userId: 'u_381', displayName: 'Dana Smith' }}
      initialDocuments={[{ source: ebook }]}
    >
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddAnnotations />
        <p className="hint">Point at a note or the green rectangle</p>
        <Stage className="stage" overlay={<HoverCard />}>
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

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

Mount it in the Stage's `overlay`, like a menu. The card stays next to the annotation while the user scrolls and
zooms, and while the annotation is moved.

`useAnnotationAnchor(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.tsx`**

```tsx
import { Anchored } from '@embedpdf/react/anchored';
import { annotationKey, useAnnotationAnchor, type Annotation } from '@embedpdf/react/annotation';
import { useApproval } from './approvals'; // your own data

// Pinned under the stamp: it stays there while the stamp moves, and scrolls away with it.
export function ApprovalStatus({ stamp }: { stamp: Annotation }) {
  const anchor = useAnnotationAnchor(stamp.ref);
  const approval = useApproval(annotationKey(stamp.ref)); // your own data

  return (
    <Anchored anchor={anchor} placement="bottom" pinned>
      <div className="status">
        {approval.signedOff ? 'Signed off' : 'Waiting'}
        <button onClick={approval.toggle}>{approval.signedOff ? 'Undo' : 'Sign off'}</button>
      </div>
    </Anchored>
  );
}
```

Mount one for each annotation that needs it in the Stage's `overlay`,
from `useAnnotationList()`. 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.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { Anchored } from '@embedpdf/react/anchored';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationAnchor,
  useAnnotationList,
  useAnnotationState,
  type Annotation,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './stamp-status.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

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.
function ApprovalStatus(props: { stamp: Annotation; signedOff: boolean; onToggle: () => void }) {
  const anchor = useAnnotationAnchor(props.stamp.ref);

  return (
    <>
      {props.signedOff && (
        <Anchored anchor={anchor} placement="top-end" gap={-12} pinned>
          <span className="check">✓</span>
        </Anchored>
      )}
      <Anchored anchor={anchor} placement="bottom" gap={8} pinned>
        <div className="status">
          <span className={props.signedOff ? 'dot dot--done' : 'dot'} />
          {props.signedOff ? 'Signed off' : 'Waiting'}
          <button
            type="button"
            className={props.signedOff ? 'sign sign--undo' : 'sign'}
            onClick={props.onToggle}
          >
            {props.signedOff ? 'Undo' : 'Sign off'}
          </button>
        </div>
      </Anchored>
    </>
  );
}

// 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, mounted in the Stage's overlay. Your own data
// for each stamp is kept by its name.
function Approvals() {
  const stamps = useAnnotationList({ subtype: 'stamp' });
  const [signedOff, setSignedOff] = useState<Record<string, boolean>>({
    'approval-budget': true,
  });

  return stamps.map((stamp) => {
    const name = approvalName(stamp);
    if (!name) return null;
    return (
      <ApprovalStatus
        key={name}
        stamp={stamp}
        signedOff={signedOff[name] ?? false}
        onToggle={() => setSignedOff((current) => ({ ...current, [name]: !current[name] }))}
      />
    );
  });
}

// 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.
function AddStamps() {
  const annotation = useAnnotation();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const cover = usePageList()[0]?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (!ready || !cover || added.current) return;
    added.current = true;
    void approvedPicture().then(async (appearance) => {
      await annotation.create(
        cover,
        {
          subtype: 'stamp',
          box: { x: 330, y: 470, width: 180, height: 60 },
          name: 'approval-budget',
        },
        { appearance },
      );
      await annotation.create(
        cover,
        {
          subtype: 'stamp',
          box: { x: 96, y: 712, width: 180, height: 60 },
          name: 'approval-contract',
        },
        { appearance },
      );
    });
  }, [annotation, ready, cover]);

  return null;
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddStamps />
        <p className="hint">Sign a stamp off, then move it: its status goes with it.</p>
        <Stage className="stage" overlay={<Approvals />}>
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

// 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 component must take clicks itself: buttons, links, a text field. Mark the
renderer `interactive`:

**`interactive.tsx`**

```tsx
import { AnnotationLayer, type AnnotationRenderer } from '@embedpdf/react/annotation';
import { ApprovalWidget } from './approval-widget';

const renderers: AnnotationRenderer[] = [
  {
    for: (annotation) => annotation.subtype === 'stamp' && annotation.name === 'Approved',
    component: ApprovalWidget,
    interactive: true,
  },
];

export const Annotations = () => <AnnotationLayer renderers={renderers} />;
```

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 component, 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.tsx`**

```tsx
import { AnnotationLayer, type AnnotationRenderer } from '@embedpdf/react/annotation';
import { ApprovalWidget } from './approval-widget';

const renderers: AnnotationRenderer[] = [
  {
    for: (annotation) => annotation.subtype === 'stamp' && annotation.name === 'Approved',
    component: ApprovalWidget,
    interactive: ({ toolId }) => toolId === 'pan', // interactive in reading mode only
  },
];

export const Annotations = () => <AnnotationLayer renderers={renderers} />;
```

## 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 `<FormLayer>` 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 `useAnnotationList()`.
