- PDF Viewer
- PDF Toolbar
- PDF More Actions Menu
- PDF Floating Toolbar
- PDF Document Tabs
- PDF View
- PDF Document Grid
- PDF Document Info
- PDF Page Navigation
- PDF Zoom Controls
- PDF Undo/Redo Buttons
- PDF Keyboard Shortcuts
- PDF Bookmark Sidebar
- PDF Thumbnail Sidebar
- PDF Search Panel
- PDF Attachment List
- PDF Annotation Layer
- PDF Annotation Toolbar
- PDF Annotation Inspector
- PDF Annotation Selection Menu
- PDF Annotation Sidebar
- PDF Comment
- PDF Comment Draft
- PDF Comment Button
- PDF Comment Sidebar
- PDF Redaction Toolbar
- PDF Capture Button
- PDF Form Fill Toggle
- PDF Signature Button
- PDF Stamp Button
Floating actions for a selected annotation — delete, plus whatever else you compose in.
"use client";
import { createPluginRegistration } from "@embedpdf/core";About#
<PdfAnnotationSelectionMenu /> is the companion to <PdfAnnotationToolbar />. The toolbar arms a tool so you can draw; this is what shows up once an annotation exists and you click it. It floats just below the selected annotation as a small toolbar of actions.
Delete is the only action that ships in the box. Everything else is composed in as children, which land to its left — so an app adds duplicate, lock, or <PdfCommentThreadButton /> without this component growing a dependency on each. See Composing actions.
It isn't rendered on its own. The annotation plugin's <PdfAnnotationLayer> owns selection and positioning, and calls this through its selectionMenu render prop — once per annotation, handing over the menuWrapperProps that place and counter-rotate the menu over the page. Your job is to forward those props and the active documentId:
<PdfAnnotationLayer
documentId={documentId}
pageIndex={pageIndex}
selectionMenu={(props) => (
<PdfAnnotationSelectionMenu documentId={documentId} {...props} />
)}
/>There is deliberately no note field here. Writing into an annotation's
contents gives a shape one unattributed, undated string that silently
overwrites whatever another tool put there. Commenting on an annotation is
replying to it — see Commenting on an
annotation.
Like the rest of the annotation components, this does nothing until
AnnotationPluginPackage is registered on <PdfViewer /> and an
<PdfAnnotationLayer> is rendered — the menu is one half of that setup, not a
standalone control.
Installation#
pnpm dlx shadcn@latest add pdfcn/annotation-selection-menu
Usage#
Register the annotation plugin.
import { createPluginRegistration } from "@embedpdf/core";
import { AnnotationPluginPackage } from "@embedpdf/plugin-annotation/react";
// Outside the component — a new array identity on every render tears the
// engine down and rebuilds it, losing scroll and zoom state.
const plugins = [createPluginRegistration(AnnotationPluginPackage)];Render the annotation layer and pass the menu to its selectionMenu render
prop.
<PdfViewer
documents={[{ url: "/sample.pdf" }]}
plugins={plugins}
className="h-[720px]"
>
<PdfToolbar>
<PdfAnnotationToolbar />
</PdfToolbar>
<PdfViewerContent
pageLayers={({ documentId, pageIndex }) => (
<PdfAnnotationLayer
documentId={documentId}
pageIndex={pageIndex}
selectionMenu={(props) => (
<PdfAnnotationSelectionMenu documentId={documentId} {...props} />
)}
/>
)}
/>
</PdfViewer>The documentId isn't part of the render-prop payload, so pass it from the pageLayers scope, where you already have it. Everything else — selected, context, menuWrapperProps, rect — is spread straight through from the layer.
Examples#
Default#
Arm a tool from the toolbar, draw an annotation, then click it: the menu appears below the shape. This example composes in a <PdfCommentThreadButton />, so you can open a conversation on the shape as well as delete it.
"use client";
import { createPluginRegistration } from "@embedpdf/core";Composing actions#
children render to the left of delete, separated from it — the toolbar pattern, so the menu grows without this component learning about every action an app wants:
<PdfAnnotationSelectionMenu documentId={documentId} {...props}>
<PdfCommentThreadButton
documentId={documentId}
annotation={props.context.annotation.object}
/>
</PdfAnnotationSelectionMenu>Anything goes in there — the menu only supplies the surface and the separator. A rough shape for your own action:
<PdfAnnotationSelectionMenu documentId={documentId} {...props}>
<Tooltip>
<TooltipTrigger
render={
<Button variant="ghost" size="icon-sm" aria-label="Duplicate">
<CopyIcon />
</Button>
}
/>
<TooltipContent>Duplicate</TooltipContent>
</Tooltip>
</PdfAnnotationSelectionMenu>The card is keyed by the selected annotation's id, so moving to another annotation mounts a fresh one rather than handing a child's half-written draft over.
Per-annotation menus#
selectionMenu is called with the annotation, so branching on what's selected is just a conditional. That's how pdfcn's viewer block gives comment pins their own thread card and everything else the generic actions:
selectionMenu={(props) =>
isPdfCommentPin(props.context.annotation.object) ? (
<PdfCommentPopover documentId={documentId} {...props} />
) : (
<PdfAnnotationSelectionMenu documentId={documentId} {...props}>
<PdfCommentThreadButton
documentId={documentId}
annotation={props.context.annotation.object}
/>
</PdfAnnotationSelectionMenu>
)
}Respecting locked annotations#
Delete reads context.structurallyLocked — set by the PDF's locked flag or a non-interactive annotation — and disables the button rather than letting the edit fail silently. It comes through context untouched; there's nothing to wire.
Composed children get the same context from the selectionMenu payload, so they can gate themselves the same way.
Accessibility#
The menu is a small toolbar of icon buttons, each with an aria-label and a tooltip. Composed children are your own controls, so labelling them is on you — match the pattern and they'll read consistently.
Selecting an annotation requires a pointer: selection happens by clicking the annotation on the page, and there's no keyboard equivalent for placing or picking annotations. Once a menu is open, its buttons are ordinary focusable buttons.
API Reference#
PdfAnnotationSelectionMenu#
A <div> carrying data-slot="pdf-annotation-selection-menu", rendered inside the layer's positioning wrapper. Accepts the layer's render-prop payload plus documentId, and every remaining div prop.
| Prop | Type | Default | Description |
|---|---|---|---|
documentId | string | — | The active document. Pass it from the pageLayers scope. |
children | React.ReactNode | — | Extra actions, rendered to the left of delete behind a separator. |
selected | boolean | — | From the layer. The menu renders null unless the annotation is selected. |
context | AnnotationSelectionContext | — | From the layer. The selected annotation plus its derived lock flags. |
menuWrapperProps | MenuWrapperProps | — | From the layer. Positioning + counter-rotation; spread onto the wrapper as-is. |
rect | Rect | — | From the layer. The annotation's viewport-scaled bounding box. |
Returns null when nothing is selected or the annotation plugin isn't registered. Delete carries data-slot="pdf-annotation-selection-menu-delete"; it calls the annotation scope's deleteAnnotation and then clears the selection, and is disabled while structurallyLocked is set.
The card stops pointerdown from escaping. The layer sits under a pointer-interaction manager that would otherwise treat a press on the menu as a press on the page, closing whatever a child just opened.