EmbedPDF

Menus over the page

A menu next to selected text, a card next to an annotation, a button over a search match: UI that belongs to a spot on the page, but floats above it. <Anchored> places your UI at a spot, and keeps it there while people scroll, zoom and rotate. Put it in the Stage’s #overlay slot:

stage-overlay.vue
<script setup lang="ts">
import { Anchored } from '@embedpdf/vue/anchored';
import { RenderLayer } from '@embedpdf/vue/render';
import type { SearchHit } from '@embedpdf/vue/search';
import { Stage } from '@embedpdf/vue/stage';
import { highlight } from './highlight';

defineProps<{ hit: SearchHit }>();
</script>

<template>
  <Stage>
    <template #page>
      <RenderLayer />
    </template>
    <template #overlay>
      <Anchored :anchor="{ page: hit.page, bounds: hit.bounds }" placement="bottom">
        <button @click="highlight(hit)">Highlight</button>
      </Anchored>
    </template>
  </Stage>
</template>

The anchor is a box on a page, in page coordinates: the same page and bounds every plugin gives you. The overlay moves with the view in the same frame as the pages, so your menu never lags behind.

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { searchPlugin } from '@embedpdf/vue/search';
import { localEngine } from '@embedpdf/engine';
import Pages from './Pages.vue';

import '../overlay.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <Pages />
    </DocumentGate>
  </Viewer>
</template>

Ready-made menus#

Most menus follow something that’s already on the page. These place themselves:

ComponentAttaches toPage
<SelectionMenu>The selected textSelecting & copying
<AnnotationMenu>The selected annotationsSelecting & editing
<AnnotationDraftMenu>A polygon or polyline being drawnSelecting & editing

They hide while people drag, and show when the selection settles. What’s inside is yours.

Attach to an annotation#

useAnnotationAnchor(ref) gives you an annotation’s anchor, and follows it while it’s moved. A count of replies next to a note stays put, so it’s pinned:

reply-count.vue
<script setup lang="ts">
import { Anchored } from '@embedpdf/vue/anchored';
import { type Annotation, useAnnotationAnchor, useCommentThread } from '@embedpdf/vue/annotation';

const props = defineProps<{ note: Annotation }>();

const anchor = useAnnotationAnchor(() => props.note.ref);
const thread = useCommentThread(() => props.note.ref);
</script>

<template>
  <Anchored :anchor="anchor" placement="right" :gap="4" pinned>
    <span class="badge">{{ thread?.replies.length ?? 0 }}</span>
  </Anchored>
</template>

Pass the ref as a function, and the anchor and the thread follow note when it changes.

Where it goes#

PropWhat it isDefault
anchor{ page, bounds }, or null to hide. avoid adds points to keep clear of, such as a rotation handlerequired
placement'top', 'bottom', 'left' or 'right' of the box, centred on that side, or lined up with its start or end: 'top-start', 'top-end', 'right-start' and so on'top'
gapThe space between the box and your UI, in pixels. Negative overlaps the box, for a badge on a corner8
pinnedStays exactly where placement puts it: no flipping, no moving to stay in viewfalse

When there’s no room on the side you chose, it flips to the other side, and it stays inside the view. That’s right for a menu. A badge or a status belongs on its spot, so pin it: it stays there and scrolls away with its box. Scroll the match close to an edge, then turn on Pinned:

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { searchPlugin } from '@embedpdf/vue/search';
import { localEngine } from '@embedpdf/engine';
import Pages from './Pages.vue';

import '../placement.css';

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

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

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <Pages />
    </DocumentGate>
  </Viewer>
</template>

Good to know#

Only what’s on screen costs anything#

Anchored UI on a page that isn’t on screen isn’t drawn, and doesn’t follow the scroll or the zoom. Mount one for every annotation that needs it; a document with hundreds costs only the ones in view.

Clicks stay in your menu#

A click in an anchored menu never reaches the page underneath, so it doesn’t clear the selection or start a drag.

In a PageView#

Inside a PageView, anchored UI is placed above everything else on your page, so a scrolling list or a card can’t cut it off.

Was this page helpful?

Your feedback goes directly to the documentation team.